Eclipse PDE plugin for Archi that exposes ArchiMate models via MCP protocol, enabling LLMs to query, analyse, and modify Enterprise Architecture models
개요
An Eclipse PDE plugin for Archi that exposes ArchiMate models through the Model Context Protocol (MCP), enabling LLMs to query, analyse, and modify enterprise architecture models through natural language. Archi MCP Server embeds an HTTP server inside Archi that speaks MCP. Once running, any MCP-compatible LLM client (Claude, Cline, LM Studio, etc.) can connect and interact with the currently open ArchiMate model — asking questions, searching elements, traversing relationships, composing view diagrams, and even creating or modifying model content. The server provides across querying, searching, creating, layout, routing, assessment, batch operations, images, specializations, and more — plus with ArchiMate reference material, workflow guides, and a viewpoint recipe library for LLMs. "What applications support the Customer Portal capability?" Searches elements, traverses relationships, and returns: "7 applications support Customer Portal: OrderService, PaymentGateway, ...
README
Archi MCP Server
An Eclipse PDE plugin for Archi that exposes ArchiMate models through the Model Context Protocol (MCP), enabling LLMs to query, analyse, and modify enterprise architecture models through natural language.
What It Does
Archi MCP Server embeds an HTTP server inside Archi that speaks MCP. Once running, any MCP-compatible LLM client (Claude, Cline, LM Studio, etc.) can connect and interact with the currently open ArchiMate model — asking questions, searching elements, traversing relationships, composing view diagrams, and even creating or modifying model content.
The server provides 70 MCP tools across querying, searching, creating, layout, routing, assessment, batch operations, images, specializations, and more — plus 14 MCP resources with ArchiMate reference material, workflow guides, and a viewpoint recipe library for LLMs.
Example conversation:
You: “What applications support the Customer Portal capability?”
LLM: Searches elements, traverses relationships, and returns: “7 applications support Customer Portal: OrderService, PaymentGateway, …”
Requirements
| Requirement | Version |
|---|---|
| Archi | 5.7+ |
| Java | 21+ |
| An MCP-compatible LLM client | Claude CLI, Cline, LM Studio, etc. |
LLM model size recommendation: 8B+ parameters minimum, 14B+ for reliable tool calling, 70B+ for complex view composition workflows.
Installation
- Download the latest
.archipluginfrom the Releases page (or thebin/directory for pre-built artifacts) - In Archi: Help > Manage Plug-ins > Install New… or copy to Archi’s
dropins/folder - Restart Archi
Getting Started
1. Start the Server
Open an ArchiMate model in Archi, then:
Menu: MCP Server > Start MCP Server
The MCP Server entry in Archi’s menu bar has three items:
| Menu item | What it does |
|---|---|
| Start MCP Server | Starts the embedded server; toggles to Stop MCP Server while running. |
| Approval Mode | Checkable toggle for the human-owned approval gate. When on, the agent’s changes queue for your review instead of applying immediately — see Mutation Safety. |
| Pending approvals (N) | Opens the Pending Approvals dock view; N is the live count of changes awaiting your decision. |
Server behaviour (port, bind address, TLS, authentication, …) is configured separately under Window > Preferences > MCP Server — see Configuration. The default endpoint is http://127.0.0.1:18090.
2. Configure Your LLM Client
Claude Code (CLI)
In your project’s .mcp.json or ~/.claude.json:
{
"mcpServers": {
"archi": {
"type": "http",
"url": "http://127.0.0.1:18090/mcp"
}
}
}
Or via the CLI:
claude mcp add --transport http archi http://127.0.0.1:18090/mcp
Claude Desktop
Claude Desktop does not natively support Streamable HTTP, so a proxy is required. Install uv (a single Rust binary), then add to your claude_desktop_config.json:
Windows:
{
"mcpServers": {
"archi": {
"command": "C:\\Users\\YOUR_USER\\.local\\bin\\uvx.exe",
"args": ["mcp-proxy", "--transport", "streamablehttp", "http://127.0.0.1:18090/mcp"]
}
}
}
macOS:
{
"mcpServers": {
"archi": {
"command": "uvx",
"args": ["mcp-proxy", "--transport", "streamablehttp", "http://127.0.0.1:18090/mcp"]
}
}
}
Cline / Other MCP Clients
Point your MCP client at the Streamable-HTTP endpoint:
http://127.0.0.1:18090/mcp
SSE transport is also available at /sse for older clients.
3. Start Querying
With the server running and your LLM client connected, you can ask questions in natural language:
- “Give me an overview of this architecture model”
- “Find all Application Services in the model”
- “What does the Order Processing component depend on?”
- “Show me the relationships between the CRM and ERP systems”
- “Create a new view showing the payment processing flow”
- “Auto-layout and route the connections on this view”
Configuration
Access via Window > Preferences > MCP Server in Archi.
| Setting | Default | Description |
|---|---|---|
| Port | 18090 |
HTTP(S) server port |
| Bind Address | 127.0.0.1 |
Network interface (localhost only by default) |
| Auto-Start | false |
Start the server automatically when Archi launches |
| Log Level | INFO |
Logging verbosity: DEBUG, INFO, WARN, ERROR |
| Enable TLS | false |
Use HTTPS with TLS encryption |
| Keystore File | (empty) | Path to PKCS12/JKS keystore (auto-generated if using self-signed) |
| Keystore Password | (empty) | Password for the keystore file, stored in your OS keychain via Equinox secure storage (no separate password to manage) — never written to disk in cleartext |
| Enable bearer-token authentication | false |
Require an Authorization: Bearer header on every request (see below) |
TLS / HTTPS
The server supports optional TLS encryption. To enable:
- In preferences, check Enable TLS (HTTPS)
- Click Generate Self-Signed Certificate to create a keystore automatically
- Restart the server — the endpoint changes to
https://127.0.0.1:18090
Clients must trust the self-signed certificate. For curl testing, use the -k flag. For LLM clients, import the certificate into the client’s trust store or the JVM cacerts.
The keystore password (whether you type it or generate it with the button) is stored in your OS keychain via Equinox secure storage — the same store the bearer token and Archi itself use — not in the plaintext preference file. If you had a keystore password set in an earlier version, it is migrated automatically on first start.
Enabling authentication (bearer token)
By default the server requires no authentication — anything that can reach the port can call the tools, which is why the default bind is loopback only. If you bind to a non-loopback address, or simply want a secret required even on loopback, enable an opt-in bearer token:
- In Window > Preferences > MCP Server > Authentication, check Enable bearer-token authentication. A 256-bit token is generated automatically on first opt-in and stored in your OS keychain (via Equinox secure storage — no separate password to manage).
- Click Copy to copy the token, or Generate / Regenerate token to roll it (regenerating invalidates clients still using the old token).
- Restart the server. Every request to
/mcpand/ssemust now sendAuthorization: Bearer; a missing, malformed, or wrong token gets401.
Configure your clients to send the header:
Claude Code (CLI):
claude mcp add --transport http archi http://127.0.0.1:18090/mcp --header "Authorization: Bearer "
Claude Code (.mcp.json / ~/.claude.json):
{
"mcpServers": {
"archi": {
"type": "http",
"url": "http://127.0.0.1:18090/mcp",
"headers": { "Authorization": "Bearer " }
}
}
}
Claude Desktop (via mcp-proxy) — pass the header through the proxy:
{
"mcpServers": {
"archi": {
"command": "uvx",
"args": ["mcp-proxy", "--transport", "streamablehttp", "--headers", "Authorization", "Bearer ", "http://127.0.0.1:18090/mcp"]
}
}
}
curl (testing):
curl -X POST http://127.0.0.1:18090/mcp -H "Authorization: Bearer " -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'
Authentication is off by default — existing configs without an Authorization header keep working unchanged until you enable it. On a non-loopback bind, also enable TLS so the token is not sent in cleartext.
Security model
The server exposes mutating and file-touching tools over HTTP, so the trust
boundary is worth understanding. SECURITY.md states what the
server protects against by default (loopback bind, Origin/Host validation,
request/session/image resource limits, UTF-8 request-body enforcement, the
human-owned approval gate) versus
what is your responsibility as the operator (transport encryption, client
authentication, securing a non-loopback bind), and how to report a
vulnerability. Read it before binding off-loopback or pointing an agent at
untrusted input.
Available Tools
The server exposes 70 MCP tools organised into functional categories.
Query & Model Inspection (6 tools)
| Tool | Description |
|---|---|
get-model-info |
Model overview — name, purpose, custom properties, element/relationship/view counts by type and layer, plus specialization count. Read counterpart to update-model |
get-element |
Retrieve element(s) by ID (single via id or batch via ids array) |
get-views |
List views with optional viewpoint type or name filtering |
get-view-contents |
View diagram contents — elements, relationships, visual positions, connection routing, image visuals, and the full styling surface (typography, gradient, alignment, line style, labelExpression) so styling mutations can be read back and verified. format: "tree" returns the containment hierarchy, descending both visual groups and ArchiMate-element containers — nested children appear in the parent node’s children array with a childCount. The group stats count both container kinds the layout family arranges — a native view group and an ArchiMate Grouping element — so topLevelGroups matches what arrange-groups will position on the canvas rather than under-reporting a Grouping-built view as flat. topLevelGroups counts depth as well as kind: a container drawn inside a host is nestedGroups, and arrange-groups arranges it anyway — inside that host, reported in nestedContainersArranged — so on such a view this stat is the lower of the two numbers. Every container node carries isGroup: true at any depth in tree and graph, format: "summary" counts both under its Containers: clause, and ungroupedElements excludes containers (a container is what loose elements are placed into). groups() itself is unchanged, so exclude: ['groups'] behaves as before. Graph nodes and edges also carry their visual identifiers (viewObjectId on nodes, viewConnectionId on edges) so a returned visual can be fed straight into remove-from-view / update-view-object / update-view-connection without a second lookup. On Archi 5.10, connections also report relativePosition — the connection “Label Offset” anchor — so an offset applied by auto-route-connections can be read back programmatically (omitted on Archi 5.7 and for the un-offset default). Connections also carry sourceRenderFace / targetRenderFace — the element face the line is actually drawn leaving and entering (top / bottom / left / right), which nothing else in the payload gives you: the anchors are element centres the renderer aims from, and a bendpoint is a waypoint it aims at, so neither is the point where the line meets the box. The field is omitted whenever a face cannot be established — a corner attachment, a reference inside the box, an attachment on a rounded figure’s arc, a zero-size element, a Junction, a Grouping on its alternate figure, or a configuration under which Archi’s two anchor algorithms would disagree — and its absence never means “no face”. absoluteBendpoints is likewise derived, not stored: Archi holds each bendpoint twice, relative to each endpoint’s centre, and the response interpolates between the two at the weight Archi renders with, (i + 1) / (n + 1) for bendpoint i of n. While the two reconstructions agree the point is exact; once an endpoint has moved they diverge, the drawn line is sheared, and the reported point follows that shear — which is the drift assess-layout reports as anchorDriftCount |
get-relationships |
Traverse relationships with configurable depth (0-3 hops) or multi-hop chain traversal with direction/type/layer filters |
find-concept-usage |
Reverse where-used lookup — given an element or relationship ID, returns every view and visual object/connection that references it. Inverse of get-view-contents. Use before delete-element / delete-relationship / rename / re-type to see the cross-view footprint in one round-trip |
Search & Discovery (4 tools)
| Tool | Description |
|---|---|
search-elements |
Full-text search across element names, documentation, and properties with optional type, layer, and specialization filters |
search-relationships |
Search all relationships by text, type, source/target element layer, and specialization — no element ID needed |
get-or-create-element |
Discovery-first — returns existing element if exact name+type match exists, otherwise creates new |
search-and-create |
Combined search + conditional create with duplicate candidate display |
Element & Relationship Creation (4 tools)
| Tool | Description |
|---|---|
create-element |
Create an ArchiMate element with type validation and duplicate detection. Optional specialization parameter auto-creates the specialization on first use. A folderId that points at a folder whose ArchiMate layer is illegal for the element type is rejected up front with FOLDER_LAYER_MISMATCH (delegated to Archi’s own folder rules) rather than failing later when Archi saves the model. Optional source map tags the element with provenance (keys auto-prefixed mcp.source.) and is honoured on every path the tool has — direct, batch-queued, approval and bulk-mutate alike. It is create-time only: to add or correct provenance afterwards, write the same keys through update-element’s properties map with the prefix spelled out. A source key that already carries the prefix, or whose prefixed form collides with a properties entry of the same name, is rejected rather than silently double-prefixed or overwriting the caller’s value |
create-relationship |
Create a relationship with ArchiMate specification rule enforcement. Optional specialization parameter auto-creates the specialization on first use. Optional type-conditional semantic attributes: accessType (AccessRelationship), associationDirected (AssociationRelationship), influenceStrength (InfluenceRelationship) |
create-view |
Create a new diagram view with optional viewpoint and connection router type |
clone-view |
Duplicate an existing view with all visual contents (elements, groups, notes, connections, bendpoints, styling). The clone references the same model objects |
Element, Relationship, View & Model Updates (4 tools)
| Tool | Description |
|---|---|
update-element |
Update element name, documentation, properties, or specialization (pass "" to clear) |
update-relationship |
Update relationship name, documentation, properties, or specialization (pass "" to clear). name and documentation also honour the empty-string clear — "" clears the field rather than meaning “leave unchanged”, on the standalone tool and inside bulk-mutate alike, and a lone {"documentation": ""} is a valid call. (update-element differs deliberately: its schema makes no empty-clear promise for those two fields, so a blank value there is ignored.) Optional type-conditional semantic attributes: accessType (AccessRelationship), associationDirected (AssociationRelationship), influenceStrength (InfluenceRelationship). Source, target, and type are immutable |
update-view |
Update view name, viewpoint, documentation, properties, or connection router type |
update-model |
Update the loaded model’s own name, purpose, and custom properties as a single undo unit. At least one of the three must be provided; omitted fields stay unchanged. Empty-string purpose clears the field; null property values remove a key |
ArchiMate Specializations (5 tools)
Specializations are IS-A subtypes of ArchiMate concept types (e.g. “Microservice” is a kind of ApplicationComponent, “Cloud Server” is a kind of Node). Use them to classify the kind of thing an element is — not for per-instance attributes like environment or version. See archimate://reference/archimate-specializations for the full guide.
| Tool | Description |
|---|---|
list-specializations |
List every specialization defined on the model with (name, conceptType, layer, usageCount, imagePath). Optional conceptType filter |
create-specialization |
Define a specialization explicitly without creating any element. Idempotent on duplicate (name, conceptType) — useful for pre-registering vocabulary at session start. Optional imagePath sets the specialization’s icon (Archi renders it on every concept of that specialization) |
update-specialization |
Rename a specialization and/or set its icon. Refuses to merge into an existing target name. Existing references move with the rename. Optional imagePath (set/change icon) and clearImagePath: true (explicit clear) — mutually exclusive. At least one of newName, imagePath, or clearImagePath must be supplied |
delete-specialization |
Delete a specialization. Refuses by default if any concept uses it; pass force: true to detach references and delete in one atomic command |
get-specialization-usage |
Audit query — lists every element and relationship referencing a specialization. Call before rename or delete |
View Composition (9 tools)
All view-composition tools that place a new visual object (add-to-view, add-group-to-view, add-note-to-view, add-view-reference-to-view, add-image-to-view) and update-view-object accept the full visual styling surface: fill / line / font colour, opacity, line width, line style (solid / dashed / dotted / none), typography (fontName, fontSize, fontStyle), gradient, deriveLineColor, outlineOpacity, figureType (rectangular / tabbed), textAlignment, verticalTextAlignment, plus note-specific borderType (dogear / rectangle / none). Existing calls are byte-identical; supply only the fields you want to set. Every one of these fields reads back through get-view-contents, so a styling mutation can be verified after the write.
Title alignment now matches the palette on the three types where Archi’s own default is a fixed constant. Archi’s diagram factory stamps a type’s UI-provider defaults onto anything drawn from the palette; this server builds objects straight from the EMF factory, which stamps nothing, so an ArchiMate Grouping, a native group and a note used to come out centre where a hand-drawn one is left — on a 400 px box, a title starting a few pixels from the edge versus one sitting ~170 px away. Those three types are now stamped left at creation, before any styling is applied, so an object created with no styling at all matches the palette while an explicit textAlignment still wins. Scope: a plain ArchiMate element’s default is derived by Archi from a user preference rather than a constant, so this server leaves it at centre and a host whose preference was changed can still differ — pass textAlignment explicitly when the alignment matters. Connections, images and view references already carried their own providers’ values and are untouched. clone-view copies both textAlignment and verticalTextAlignment, so a cloned object keeps its title band rather than silently reverting to top.
When add-to-view or add-group-to-view nests a child inside a container whose fill colour you have not explicitly set, the parent’s fill auto-recedes to a subtle backdrop (#F4F4F4) so the nesting reads as depth instead of a flat block. The recession is provenance-gated and idempotent (only an unauthored fill is touched; an explicitly coloured container is left untouched), rides the placement as one undo unit, and excludes the root view. Pass recede: false to suppress it for a call.
A view object can also anchor to a target container via update-view-object, recording an {anchorTarget, anchorEdge, dx, dy} so its position resolves from the target’s current bounds at commit time rather than a frozen snapshot — edge below (default) tracks the target’s growing bottom, above/right/left the corresponding edge. When a later mutation moves or grows the target, every anchored object is repositioned within the same undo unit. Target and object must share a coordinate space; self-anchors and cross-space anchors are rejected. add-note-to-view’s content-relative placement uses the same edge resolver.
| Tool | Description |
|---|---|
add-to-view |
Place a model element onto a view diagram (same element can appear on multiple views). Optional imagePath, imagePosition, showIcon for custom icon overlays, labelExpression for a per-view-object dynamic label template (e.g. "${name}", "${property:Owner}"), and recede: false to suppress the auto-backdrop when nesting inside an unauthored-fill container |
add-group-to-view |
Add a visual grouping rectangle (pure visual container, no model representation). Optional imagePath, imagePosition, showIcon for custom icon overlays, and recede: false to suppress the auto-backdrop when nesting inside an unauthored-fill container. Pass label: "" to create an untitled group — use that for a container your source material never named, rather than inventing a placeholder; Archi renders it with an empty tab and substitutes nothing. The key itself is still required: omitting it is an error |
add-note-to-view |
Add a text note annotation (pure visual, no model representation). Optional imagePath, imagePosition, showIcon, and note-specific borderType. Omit height and the server fits it to the note’s wrapped text by real glyph measurement, with 80 as the floor and 600 as the cap — 80 is what short content resolves to, not a fixed default; supply a height only to pin a fixed size. Pass content: "" to create an empty note — a shaped placeholder you fill in later. Placing the annotation onto a rectangle that a route already drawn on the view passes through returns an ANNOTATION_PLACED_IN_ROUTED_CORRIDOR structured warning naming every crossed connection, on the immediate, batched and approval arms alike. Placing after auto-route-connections, as the ordering guidance says, does not avoid this — after routing is when the corridors exist to land in — so the disclosure is a measurement of this rectangle against these routes rather than more advice. A crossing here lands in the informational connectionThroughNoteCount, which caps the view at good. |
add-view-reference-to-view |
Embed another ArchiMate view as a clickable thumbnail (the agent-driven equivalent of Archi GUI’s drag-view-onto-view). Requires viewId (target) and referencedViewId (source). The referenced view’s name is not stored on the visual — renaming the referenced view auto-updates every embedding. Placing the annotation onto a rectangle that a route already drawn on the view passes through returns an ANNOTATION_PLACED_IN_ROUTED_CORRIDOR structured warning naming every crossed connection, on the immediate, batched and approval arms alike. Placing after auto-route-connections, as the ordering guidance says, does not avoid this — after routing is when the corridors exist to land in — so the disclosure is a measurement of this rectangle against these routes rather than more advice. A crossing here lands in the rated connectionPassThroughs, which can drive the view to poor — unlike a note, this visual is an ordinary layout node to the assessor. |
add-image-to-view |
Add a standalone image as a first-class diagram node (sibling to notes, groups, and view-references). Requires viewId and an imagePath returned by add-image-to-model or list-model-images. Default size is the image’s natural dimensions; typo’d paths are rejected with IMAGE_NOT_FOUND. Distinct from update-view-object’s imagePath, which sets an icon overlay on an existing element. Placing the annotation onto a rectangle that a route already drawn on the view passes through returns an ANNOTATION_PLACED_IN_ROUTED_CORRIDOR structured warning naming every crossed connection, on the immediate, batched and approval arms alike. Placing after auto-route-connections, as the ordering guidance says, does not avoid this — after routing is when the corridors exist to land in — so the disclosure is a measurement of this rectangle against these routes rather than more advice. A crossing here lands in the rated connectionPassThroughs, which can drive the view to poor — unlike a note, this visual is an ordinary layout node to the assessor. |
add-connection-to-view |
Add a visual connection representing an existing model relationship, with optional styling, label suppression, and label positioning |
update-view-object |
Update position, size, styling, image, labelExpression, and/or anchor of a visual element on a view. Optional anchoring (anchorTarget, anchorEdge below/above/right/left — default below, anchorDx/anchorDy) makes the object follow a target container’s edge, resolved from the target’s current bounds at commit time instead of a frozen absolute position — so a note stays below a group as the group grows. Target and object must share a coordinate space; pass an empty anchorTarget to clear. text sets a group’s label or a note’s content; pass text: "" to clear it, leaving the group untitled or the note empty. Omitted and empty are different requests — omitting leaves the text unchanged. An empty string is still rejected for element view objects, where update-element is the tool that renames a concept. Note height is the one exception to “omitted means unchanged”: a request that changes a note’s text or width while omitting height re-fits the height to the wrapped content — the same auto-fit add-note-to-view applies, so the same content at the same width resolves to the same number on either path. A request that only moves the note never re-fits it, and supplying height pins a fixed size |
update-view-connection |
Replace bendpoints, update styling, toggle label visibility, and/or set label position of a connection on a view. lineStyle is rejected on a connection rather than silently ignored: an ArchiMate connection carries no settable line style in Archi — its dash is derived from the relationship type at render time — so the refusal names the reason and points at lineColor + lineWidth, which do apply. (View-object lineStyle, on update-view-object, is unaffected) |
apply-positions |
Apply a complete visual layout atomically (up to 10,000 entries per call). Carries the same note exception as update-view-object: supplying a note’s width while omitting height re-fits the height to the wrapped text. Containers grow with their contents: a position that pushes an object past its container’s bounds re-fits that container and every ancestor above it, and carries along any object anchored to something the call repositions. Each container is measured once per call against what the same call has already decided about it, so two entries needing different axes of one group both survive and the arrival order does not change the result — and the re-fits reach the model as one command per group rather than one per entry that grew it. They are committed after every position lands, so naming a container in positions and asking for it smaller than its children need loses to the fit, while asking for it bigger wins and retires the fit. The response reports counts only, never the geometry those containers ended at — re-read with get-view-contents before computing from a container’s size |
View Cleanup (2 tools)
| Tool | Description |
|---|---|
remove-from-view |
Remove a visual element or connection from a view (model object preserved) |
clear-view |
Remove all visual elements and connections from a view (model objects preserved) |
Layout & Routing (11 tools)
LLM agents: Fetch
archimate://prompts/routing-preconditions-checklistbefore invokingauto-route-connectionsorauto-layout-and-routeon any non-trivial view. The routing pipeline cannot recover from missing preconditions — it can only route the geometry the agent has set up. The spacing convenience tools self-terminate honestly: if a view is provably too dense for spacing to fix they returnterminationReason: density_floor_reflow_requiredand offer a user-consentable structural reflow — surface that to the user instead of looping the spacing tools.
| Tool | Description |
|---|---|
auto-route-connections |
Orthogonal connection routing using clearance-weighted visibility-graph A* pathfinding with corridor directionality, corridor diversity, group-wall awareness, channel-global ordered nudging, and post-routing path straightening. Two modes: mode: "full" (default) re-routes whole connections via visibility-graph A*; mode: "terminals-only" rectifies only the first/last bendpoint of each connection to make terminal segments orthogonal — best after ELK on grouped views to fix diagonal terminal entries without the crossing inflation a full re-route causes; it also sweeps redundant interior collinear bendpoints (a terminal L-bend that lands collinear with the existing trunk) so a re-route drives connectionRedundantBendpointCount toward zero without touching the pinned terminal egress anchors. Both modes now clear an off-face terminal hug — a connection that leaves an element face then runs a pixel or two parallel hugging it — by pushing the first trunk clear of the face: the full pipeline uses a dual-axis healthy-floor egress pass (a 15 px healthy parallel gap enforced on both axes, plus an own-face short-circuit that catches sub-10 px micro-hugs), and terminals-only mirrors the same floor as pure geometry with a per-side orthogonality backstop that declines any lift which would introduce a diagonal (detection fires at the assessor’s 8 px oracle, so an already-clear terminal is byte-identical). This drives the offFaceParallelTerminalCount signal to zero. A gated final pass also dissolves a coincident same-face hub port — two terminals a downstream stage collapsed onto one perimeter port of a low-degree element face (the coincidentFacePortCount defect assess-layout reports) — by moving whichever terminal can move to a free along-face slot; it is a byte-identical no-op unless there is a real collision with a clear slot, so it never disturbs a dense hub’s own distribution or adds a crossing. Optional autoNudge mode automatically moves blocking elements (and resizes parent groups to contain them) and re-routes failed connections in a single atomic operation; a nudged child is floored at its parent’s title band rather than at the container padding, so a routing lift can never push a child under its parent’s title (the floor applies to an ArchiMate-element container as well as a visual group, and never exceeds the child’s own pre-nudge position, so it can only shorten an upward move). On Archi 5.10, when a Middle label still renders on its own endpoint box after position selection, the router applies the connection “Label Offset” (relativePosition) to lift it clear — the only channel that can clear own-endpoint label bleed on a position-preserving route (runtime-guarded; a silent no-op on Archi 5.7). Optional labelPolicy: "auto-hide-on-collision" hides exactly those connection labels that collide at every candidate position — the router already scores all three positions per connection, so this acts on a measured verdict rather than a guess — and lists each hide in hiddenLabels by connectionId with a reason, never truncated, so every one is auditable and reversible with update-view-connection(showLabel: true). It is off by default; omitting it leaves label visibility byte-identical. A label you hid yourself is never overridden nor claimed as a policy hide, the hides ride the routing pass’s own compound so one undo reverts them with the routes, and a Middle label the Archi 5.10 Label Offset can still lift clear is not treated as unplaceable. The response carries a structuredWarnings: List field for deterministic LLM iteration (AUTO_NUDGE_SKIPPED_SIBLING_OVERLAP instructs the agent to run layout-within-group on the parent before re-routing; EGRESS_LIFT_LAYOUT_BOUND reports that the router generated then rolled back off-face egress lifts because applying them would narrow a parallel-connection gap below the 15 px healthy floor — the residual hug is layout-bound, so a matching nextSteps entry names spreading the elements instead of declining silently; AUTO_ROUTE_CROSSINGS_REGRESSED reports that a full re-route increased the view’s own edge-crossing count, naming both the before and after figures and pointing at mode: "terminals-only" — it is gated on the input having been routed, so a first-time route of an unrouted view is never advised to undo a good result, and its remediationTool names a recovery of this call rather than the tool that caused the regression, scoped to the arm the call took: undo once the re-route has been applied, end-batch (with rollback:true to discard it) while it is queued in an open batch, and no tool at all while it awaits a human’s decision, since rejecting it in Archi is what restores the previous paths; CONNECTION_NOT_FOUND aggregates every unresolved id from connectionIds in remediationViolatorIds, so the matching next step now fires only when connection ids genuinely went missing instead of on any warning at all; CONNECTION_ROUTED_THROUGH_NOTE discloses every (connection, note) pair this call routed straight through a note — a note is excluded from the A* obstacle set on purpose, so the crossing is a disclosure and not a violation, and the code exists to name which note was crossed on the call that crossed it instead of leaving that to a follow-up assess-layout; its remediationViolatorIds carry the note ids and its remediationTool is update-view-object, because the note is what the caller moves, not the route; AUTO_NUDGE_NET_ZERO reports an element that autoNudge moved and that ended at the position it started from — such an element is no longer listed in nudgedElements nor counted in nextSteps, because reporting it as nudged contradicted the same response’s failed array and its recommendations, which repeated the identical move. Two causes produce that zero — a move a later iteration reversed, and one the parent-containment clamp absorbed — and the summed deltas cannot tell them apart, so the message names the outcome and says so rather than claiming the loop oscillated; the remedy is a spacing lever, never another route). Terminals are held on the element face the router assigned them for the whole pipeline: a terminal displaced by a clearance stage’s micro-jog cleanup is realigned a second time immediately after that cleanup, a terminal whose own segment crosses an obstacle is slid along its own face to align with its neighbour (clamped to the face’s extent, kept only if that clears the crossing) rather than deleted outright with its neighbour promoted to the terminal index on whatever off-face coordinate it happened to hold, and a stage that deliberately re-selects a face re-derives the anchoring record instead of leaving it naming the face the terminal just left. Every one of those refuses to guess in the same direction — a terminal on no face line, or exactly on a corner where two face lines hold equally, yields no face rather than an invented one |
auto-layout-and-route |
Two modes: auto (default) uses ELK Layered to compute positions AND routes in one operation; grouped orchestrates the full grouped-view workflow (layout-within-group + arrange-groups + optimize-group-order + auto-route-connections) atomically. grouped requires the view to have at least one top-level container, and both container kinds qualify — a native view group and an ArchiMate Grouping element — so a Grouping-built view is a valid target rather than being rejected as having no groups. grouped lays out the whole subtree, not one level: each top-level group is delegated to the same post-order recursion layout-within-group’s recursiveChildren drives, so a child that is itself a container is sized from its contents (with its title band reserved) instead of from its own label text and then descended into — elementsRepositioned counts every descendant moved, each re-fitted nested container is reported in nestedContainersFitted with the rectangle it landed at, and the depth cap surfaces as a response flag. Smart iteration when targetRating is set — factor-aware iteration tunes the right knob per tier, with plateau detection to exit early when iterations stop improving the dominant tier, and a terminationReason on the response saying which of the loop’s stopping conditions actually fired. labelPolicy is honoured wherever a routing pass runs (grouped mode, and flat mode with a targetRating); flat mode without a targetRating takes ELK’s own edge routes and never invokes the label optimizer, so it rejects the parameter with a remedy rather than accepting it and doing nothing. The loop keeps the best attempt it produced, which can still be worse than the view you handed it — so every quality-loop run reports ratingBefore (the rating measured before any mutation) beside achievedRating, and a committed result that is worse raises an AUTO_LAYOUT_RATING_REGRESSED structured warning naming each metric that moved with before/after values, remediationTool: undo. It is not gated on the target being missed: meeting a target is at least as good as, so an already-excellent view can be downgraded to a good target, report goal reached, and still be a regression. Do not read achievedRating alone as evidence the call helped |
layout-within-group |
Arrange child elements inside a container using row, column, or grid patterns. The container may be a visual group or an ArchiMate-element container (ApplicationComponent, Node, ApplicationFunction, etc.) that holds nested children — the same arrangement / spacing / padding / columns / autoResize / autoWidth semantics apply to both. recursive is the exception: the upward pass walks native view groups only, so it neither starts from nor continues past an ArchiMate-element container. That is deliberate — growing an element as a side effect of arranging its child is a semantic act, not a geometric one — and it is no longer silent: ancestorPropagation ships on every response, saying as a stable code which situation produced the count you got — including the two that can re-fit some ancestors and then stop at a parent they could not grow, where a positive count reads as success and is not. Optional recursiveChildren descends into nested containers, arranging every level post-order in one atomic command with a single undo — for multi-level inventories (functions in components in domains, or region → availability zone → node → artifact) that otherwise need hand-computed coordinates. Under the recursive descent each grid column is sized from its own widest member rather than from the widest element in the whole grid, so one over-wide descendant no longer inflates its siblings, their container, and that container’s siblings a level up; columns still line up across rows, and the non-recursive default is unchanged. The response reports what the pass actually did in both directions: resizedAncestors / ancestorsResized name the containers it grew upward, nestedContainersFitted names every descendant container it re-fitted downward (id plus landed rectangle, omitted when empty), resizedElements names every leaf child whose size actually changed with the rectangle it ended at — this call writes a full rectangle to every child and in a grid that rectangle is its cell, so a narrow element takes its column’s width whether or not you set autoWidth, and the move count alone never disclosed it — the depth cap surfaces as a response flag, and groupResized reflects a measured change rather than the autoResize you asked for, so a repeated layout that moves nothing honestly returns false. Notes, view-references, and connections are rejected as containers |
layout-flat-view |
Automatic layout for flat (non-grouped) views — row, column, or grid arrangement with optional sorting by name/type/layer and category grouping |
arrange-groups |
Position top-level containers relative to each other in grid, row, or column layout. Two kinds of container are arranged and both count as a “group” here: a native view group from add-group-to-view, and an ArchiMate Grouping element from create-element + add-to-view — so a view built entirely from Grouping zones (the shape the technology and deployment recipes prescribe) is arranged like any other. Density-aware default: when spacing is omitted on a view with inter-group connections, the tool derives a connection-count-aware default (≤ 15 → 80 px, 16–30 → 100 px, > 30 → 120 px). Pass an explicit spacing to suppress. The response reports positionedContainers — each container’s id and the rectangle it actually landed at, read back after the write — beside the groupsPositioned count, so a container the pass never touched is visible rather than hidden inside a number. It also accounts for every direct child of the view: skippedContainers names each populated top-level container left standing with the type that made it ineligible, unhandled closes the population between the buckets, and groupsPositioned + standaloneElementsPlaced + skippedContainers + unhandled equals the view’s direct-child count — an invariant the build enforces, so a shortfall can no longer hide inside a count with nothing to sum against. Naming a container’s viewObjectId in groupIds arranges it alongside the zones whatever its element type (you can see the canvas and this tool cannot), which counts it in groupsPositioned, returns its rectangle in positionedContainers and removes it from skippedContainers. A wrong groupIds entry is reported on its own rather than voiding the others. A container drawn inside a host is arranged too, and this is a visible behaviour change: a Grouping inside a Node typing a cloud region has no zone above it, so it is top-level — the view is no longer refused with No top-level groups found. It cannot join the canvas grid, because its coordinates are stored relative to the host, so it is arranged inside that host, in the host’s own coordinate space, and reported in nestedContainersArranged with the hostViewObjectId those coordinates are measured from. That field sits outside the four-bucket identity, which still partitions the view’s direct children exactly. A host too small for the arrangement is declined whole — this tool never grows an ArchiMate element to make room — and the host itself is still listed in skippedContainers, with a reason saying the containers inside it were arranged. A container that is a member of another arrangement target is still denied as not top-level rather than not found: it moves with the target holding it |
optimize-group-order |
Reorder elements within groups to minimise inter-group edge crossings. Works on all top-level containers in the view simultaneously — native view groups and ArchiMate Grouping elements alike. That parity is about container kind; on depth it is narrower than arrange-groups, reordering the view’s own containers only. A view whose containers are all drawn inside a host is refused with a message that says so and points at arrange-groups, rather than the former View has no groups |
resize-elements-to-fit |
Resize all (or selected) elements on a view to fit their labels using SWT font metrics. Two-pass algorithm for nested containment: children first, then parents. Optional wrapFit: true keeps each element’s width and grows only its height so a long label wraps onto extra lines instead of being clipped or forced into an over-wide box — useful for nested labels in fixed-width containers (after a parent grows to contain a wrapped child, follow with auto-route-connections). Sizes for label legibility only — not for connection fan-out. For hub elements with high connection counts, use detect-hub-elements plus update-view-object. When a resize displaces objects anchored to its target, the enclosing group is fitted around the rectangles those children land on, not just around the object you named — so an anchored child can no longer be left hanging outside the group that was grown for it (the fit runs once, after every target in the pass has landed). The response reports both the groups it grew and the objects it displaced |
adjust-view-spacing |
Inflate inter-element and inter-group spacing on an existing view, then re-route in a single atomic operation. Use when a view is correctly laid out but visually cramped, without re-running ELK from scratch and losing manual placement intent. A container here is either a native view group or an ArchiMate Grouping element; both are inflated. Density-aware default: when interElementDelta is omitted on a view with coincidentSegmentCount > 2 or connectionEdgeCoincidenceCount > 4, the tool derives a connection-count-aware default (≤ 15 → 60 px target, 16–30 → 80 px, > 30 → 100 px). Pass interElementDelta: 0 to suppress. After spacing inflation, a post-pass detects any child element that overflows its parent group bounds and resizes the parent |
apply-element-spacing-recommendations |
Convenience tool that runs an embedded observe → decide → density-aware-terminate control loop to inflate within-group element spacing. Per iteration it takes a small monotone step, re-runs assess-layout, and continues / escalates / stops; a degrading step is reverted as the loop’s own six-input step scalar measures it — that scalar cannot see cousin overlaps, off-canvas placement or overallRating, so the tool can return a view rated worse than the one it was handed and reports that as a SPACING_RATING_REGRESSED structured warning rather than leaving it silent. Hub-aware tier selection (80/100/120 px) when some element on the view carries more than 6 connections. Note this is a higher cut than the ≥ 5 hub-candidate threshold below: a non-empty detect-hub-elements result means only that the view is connected, not that it has a large hub. Returns before/after assess-layout snapshots plus terminationReason / iterationCount / appliedDeltas[]. One call = one undo step. Set dryRun: true to preview |
apply-group-spacing-recommendations |
Sibling-symmetric convenience tool that runs the same embedded control loop to widen inter-group corridors only — preserves group ordering and topology. Hub-aware connected-pair tier rises to 100/140/160 px. Returns before/after assess-layout snapshots plus terminationReason / iterationCount / appliedDeltas[]. dryRun: true previews. With hub sizing and apply-element-spacing-recommendations this completes the routing-preconditions triad |
apply-spacing-recommendations |
Composed convenience tool that runs TWO coordinated control loops (element arm, then inter-group arm) in one transactional call. The scope parameter (both / element / group) selects which arm(s) run. The inflation-knee constants are per-iteration step caps (+80 px element / +100 px inter-group per iteration), preventing cumulative-inflation-past-the-knee without a fixed per-call ceiling; elementKneeClampApplied / groupKneeClampApplied surface when a step cap fires. Reports per-arm terminationReason / iteration counts / applied deltas. Use when both axes need adjustment; single-arm siblings cover single-axis changes |
Layout Assessment & Analysis (2 tools)
Metric acronyms below — M1–M6, R8,
parallelConnectionGap_V_p10(V_p10), HPQ — are defined in the glossary.
| Tool | Description |
|---|---|
assess-layout |
Assess view layout quality across the legacy 8-metric severity-tiered rating (overlaps, crossings, pass-throughs, coincident segments, non-orthogonal terminals, spacing, alignment, label overlaps) plus the perception-aligned metrics (M1 visible-segment-length non-orthogonal terminals, M2 interior terminations, M3 zigzags, M4 connection-vs-edge coincidence, M5 hub-port quality, R8 corridor utilisation) and the M6 two-dimensional rating (layout tier × routing tier). Also reports the informational parallelConnectionGap_V_p10 signal (10th-percentile parallel-segment gap on the V axis, anchored at 13.30 px on the ArchiMate reference) for narrow-corridor regime detection. Informational detections cover label truncation, parent-label obscured by child, image sibling overlap (imageSiblingOverlapCount — now also examines specialization icons and sizes images from their true pixel dimensions, clipped to the element box, because Archi cuts an oversized image off at the box edge rather than drawing it outside, so an icon’s phantom overhang no longer generates image crossings, border grazes or sibling overlaps in space where nothing is drawn), overlay-icon collisions across a containment pair (overlayIconCollisionCount / overlayIconCollisionDescriptions — a container’s corner icon overlapping a nested child’s own corner icon, which the parent-bucketed sibling detector above is structurally unable to see; fill images excluded on both sides, informational with zero rating impact), clipped notes (noteClipCount — a note whose text needs more height than its box, typically an explicit height that defeats auto-fit), and an element’s own icon drawn over its own title (ownIconOverLabelCount / ownIconOverLabelDescriptions — the third icon axis, structurally invisible to the two above because the icon rect and the title share one element box; the remedy is per view object, so it must be repeated on every view showing the element). Being informational governs the rating, not the prose: every informational count is named in suggestions on any run where it is nonzero — with the remedy published for it and a pointer to the field or violator key listing the objects — and ownIconOverLabelCount is additionally named in nextSteps, while none of them enters a rating. A finding that no suggestion accounted for is named inline with its count by a terminal disclosure, whether or not other defects were reported; the mapping is recorded as each suggestion is added rather than read from a table of which metrics have remedies, because a threshold-gated remedy (edgeCrossings fires only above ten) is unexplained on the runs below its threshold. Connection-label overlap detection is render-calibrated (glyph box widened 1.35× to match Archi’s rendering) and now flags a label rendered on its own endpoint box (the channel auto-route-connections clears via the Archi 5.10 Label Offset), while correctly ignoring labels suppressed with showLabel: false and labels already lifted clear by an applied Label Offset (offset-aware). The own-endpoint test combines an area-fraction rule with a box-coverage rule (catching a label blanketing a tiny endpoint box such as a junction), a short-segment promotion (lowering the bar when the label is wider than the first/last segment it anchors to), and a junction near-zero bar (a Junction renders as a solid dark shape with no readable interior, so a label grazing its own junction endpoint is flagged at ~5% — catching an oversized junction the box-coverage rule, which only fires on a tiny one, misses). A rating-affecting hubNeighbourClearanceMin (px) reports the tightest hub-edge-to-spoke-row clearance on any crowded hub face; a value below the 60 px crowding floor caps the layout tier so a hub enlarged until it crowds its neighbours can no longer rate good (-1.0 = no measurable hub, no rating impact). Two route-quality detections affect the routing tier: connectionThroughNoteCount flags a connection routed straight through a note’s box or an element’s rendered image rectangle (binary-presence Tier-3R cap-good; complements the box-based connectionPassThroughs, which the routing tier takes the worse of so there is no double penalty), and nonOrthogonalInteriorSegmentCount extends the non-orthogonal-terminal check to mid-route segments (Tier-2R cap-fair, ratio-bucketed like its terminal sibling and combined with it by the worse). The terminal count is also published as its two disjoint halves — zeroBendpointNonOrthogonalTerminalCount (connections drawn as a straight line between two element centres, the ELK auto-layout signature, with no stored route to preserve) and routedNonOrthogonalTerminalCount (connections carrying stored bendpoints) — which sum to the unchanged nonOrthogonalTerminalCount, with matching nonOrthogonalTerminalsZeroBendpoint / nonOrthogonalTerminalsRouted violator keys beside the unchanged whole-population nonOrthogonalTerminals key. Each half is MEASURED where its connection is flagged rather than derived by subtracting the other from the total, which matters because the source and target terminal tests guard against different rectangles: a bendpoint-free diagonal suppressed on the source side and flagged on the target side would otherwise be filed as routed. The split is published because the halves have opposite remedies — the straight-line half is cleared with mode='terminals-only' and a full re-route would likely increase its crossings, while the routed half is re-routed by passing exactly its IDs to auto-route-connections as connectionIds, which confines the call to that half and leaves every other connection’s bendpoints untouched. Two informational detections cover the route defects that survive a geometry change: anchorDriftCount / anchorDriftDescriptions flags a connection whose stored route no longer matches the geometry it was computed for — every bendpoint is stored twice, relative to the source centre and relative to the target centre, and both describe the same point at the moment the route was written, so moving or resizing an endpoint afterwards leaves the two reconstructions disagreeing by however far the element travelled (flagged above a 1 px representable-precision floor, with the measured drift reported per axis rather than as a flag). No shape-based dimension can see this and none ever could: the collector blends the two reconstructions into the single polyline every detector is handed, so the disagreement is structurally invisible rather than under-tuned — and the stored shape was correct when written, which is why the remedy names re-routing and deliberately does not name the straightener. That blend now uses Archi’s own render weight, (i + 1) / (n + 1), so all 21 path-reading detectors measure the polyline a reader actually sees: while a connection is drifted the drawn line is sheared rather than displaced — each point pushed off the halfway position by (weight − 0.5) × drift, furthest at the two terminal bendpoints, which is exactly where the terminal-geometry dimensions look. The collector keeps its element centres untruncated (rounding them would quantise away the sub-pixel disagreement anchorDriftCount exists to report), so the same bendpoint can differ between assess-layout and get-view-contents by up to 0.5 + n/(n + 1) px — 1.0 at one bendpoint, 1.3 at four, approaching but never reaching 1.5. lateralJogReversalCount / lateralJogReversalDescriptions flags a connection that doubles back through a sidestep too narrow to be routing around anything — a four-point window whose two outer arms run in opposite directions along one axis, separated by a perpendicular jog of at most 8 px (mirroring OFF_FACE_MIN_STUB_PX, the clearance that already separates a hugging exit from a legitimate one) — reporting the four coordinates of each window. It is distinct from zigzagCount rather than a looser version of it: M3 needs three consecutive points sharing one axis, and the sidestep puts the two arms on two parallel lines, so no triple in the window shares an axis at any jog width. A connection already reported as a pass-through or a zigzag is skipped, so no route is counted under two reversal dimensions. Two further informational detections cover redundant collinear bendpoints (connectionRedundantBendpointCount — a removable point on a horizontal or vertical run whose deletion would not change the rendered orthogonal shape, distinct from zigzagCount; near-collinear diagonal micro-jogs and router-pinned terminal egress-stub ports are excluded, so a nonzero count means a genuinely collapsible interior point that a terminals-only re-route removes) and same-colour container/child “flat-blob” fills (containerFillEqualsChildCount — backstop for the auto-recede backdrop, firing only on an authored same-colour fill). Three further informational detections close the label/border blind spots: connectionGrazesVisualCount flags a connection touching or clipping a note’s or image’s border band (the outer strip the through-visual interior test discards, including visuals too small to inset — disjoint from connectionThroughNoteCount, so a crossing is classified as exactly one of through or graze); labelOnNoteCount flags a connection label rendered on a note’s rectangle (the caption collision the route detectors cannot see, since a label is positioned independently of the line); and labelOnGroupCount flags a connection label on a visual group’s title band (the title-strip collision the label-overlap detector skips, as it treats groups as transparent containers — a label in the group body is not flagged). Two more detections sharpen the terminal/edge picture — the first of them rating-bearing, the second informational: offFaceParallelTerminalCount flags a connection whose terminal route departs an element face then runs parallel hugging it (the first exterior segment travels along the departed face within 8 px of it) — the sub-pixel-off exit stub the visible-length guard suppresses in nonOrthogonalTerminalCount, resolved via the same terminal-slot face attribution, and the oracle auto-route-connections drives to zero. It is now rating-bearing: a Tier-2R entry capped at fair on binary presence, so a view carrying a real face-hug can no longer read good at the headline (ratio-bucketing was rejected — a low-ratio hug would still read good). It is deliberately not carved out of overallExcludingAcceptedCosmetics: a layout-bound hug is a real, actionable defect, not an accepted cosmetic; coincidentFacePortCount flags an element face on which two or more connection terminals collide onto the same perimeter port (two edges appear to leave one point) — the blind spot in hubPortQualityScore (M5), whose per-face guard never scores a face carrying only two or three coincident connections, so it reads a vacuous 1.0 despite the collision (informational, M5 untouched; the oracle the router’s coincident-port dissolution drives to zero); and the M4 edge-coincidence detector now enumerates every grazed element via edgeCoincidenceGrazedElementCount with an edgeCoincidenceGrazedElements element-id violator key, while the rating-bearing connectionEdgeCoincidenceCount and its connection-id edgeCoincidence key stay byte-identical (counted once per connection). The headline rating is also de-noised: ratingBreakdown.overallExcludingAcceptedCosmetics recomputes overallRating with the nonOrthogonalTerminals contribution removed (diagonal terminal segments are the straight-line signature of ELK auto-layout), as a floor that can only equal or improve overallRating — when the two differ, the gap is terminal cosmetics only (clear with auto-route-connections mode terminals-only, or accept); when equal, the rating reflects a real defect. A coverage map declares, per defect dimension, whether the run actually evaluated it (checked — detector ran and fully covers the dimension; partial — covers only some failure modes, so render-verify the rest; not-checked — no detector, absence is unknown; not-applicable), so a done-gate can tell “checked and clean” from “partially checked” from “never looked” by reading both coverage and ratingBreakdown; informational, never affects a rating. Most dimensions report a fixed registry level; the exceptions come in two kinds. Contextual — declared checked, downgraded to partial only on a run that triggers them: labelOverlaps on any run where a label is wider than its hosting segment (a label can crowd a neighbour box while still clearing it geometrically, so the overlap count is honestly zero yet the crowding mode is unverified), ownIconOverLabel on any run carrying a named group with an overlay icon (a group’s title band has no measured width to test against, so that object was never examined), and parentLabelObscured on any run carrying a parent whose title band width was never measured — a visual group, or an element whose label measurement failed (that parent is examined and does get a verdict, but its band is sized as a single line however long the title is, so a title that wraps onto further rows is compared against only its first row and a child under the rest is not flagged; render-verify such a parent’s title against its topmost child). Permanent — reported on every run because the gap is structural: edgeCoincidence is always partial (its detector classifies each segment as horizontal or vertical and skips everything else, so a diagonal segment is never compared against an element edge); labelTruncations is always partial (a visual group’s title is never measured and the node is skipped before its box is consulted, as is an element whose label width could not be measured — so a zero certifies only that every measured element label fits, and a done-gate must render-verify group titles); and corridorCentering is a standing not-checked, making explicit that corridorUtilisationScore measures multi-occupant corridor spread, not whether a single route centres versus hugs an edge. M3 (zigzag) skips connections already classified as pass-throughs so a single connection is not double-labelled. Self-element pass-throughs are reported but excluded from rating. Two overlap/boundary readings are worth knowing apart: overlapCount is a same-parent count, so a collision between two objects in different branches of the containment tree is reported through their containers rather than through the objects themselves — the informational cousinOverlapCount / cousinOverlaps (with its own violator key) names every pair of differently-parented, non-nested objects whose rectangles intersect, counted in the same pass and carrying no rating weight; and boundaryViolations is a capped description list, so boundaryViolationCount is the true uncapped number and is the one the rating, the Tier-1L regression veto and the iteration loop read. connectionPassThroughs is likewise a capped description list and additionally names the self-element pass-throughs the rating does not charge, so crossElementPassThroughCount is the uncapped cross-element number, and it is the one the rating, the Tier-1R regression veto, the iteration score and the spacing loop’s step scalar read. A degenerate view (at most one object) still does not receive a rating — spacing and alignment have no data below two objects, and scoring them would rate a pristine one-object view fair for having nothing to compare against — but it is no longer silent: the detectors that are computable on a single object (offCanvas, labelTruncations, ownIconOverLabel, noteClip) run and report, and every coverage dimension is declared, not-applicable only where the failure mode structurally needs two or more objects. The nextSteps[] envelope names the right precondition tool with violator IDs attached when an overlap, boundary violation, low hub-port-quality score, or grouped-view spacing/crossing breach is detected — and, for a near-saturated container-nested-hub view, emits a single diagnostic resize-vs-reposition step (gated on the clearance reading) instead of generic spacing inflation. The off-face parallel-terminal remedy is router-consistent and honest: a confident layout fix when the local corridor is below the healthy floor, otherwise a deferral to auto-route-connections (cross-referencing the EGRESS_LIFT_LAYOUT_BOUND warning) rather than over-promising a perpendicular re-route, and on a contested hub face (two or more connections sharing it) it names spreading the connections rather than a plain widen. Both container kinds are assessed as containers. Every detector that reasons about what the canvas shows — hasGroups, alignment participation, corridor walls, hub detection, pass-throughs, image-vs-connection, label overlaps, label-position exhaustion, labelOnGroupCount, own-endpoint overlap fractions, note containment and the pre-route stacked-position warning — treats a native view group and an ArchiMate Grouping element alike, so a view of Grouping zones is no longer reported as having no groups while its zone rectangles are counted as pass-throughs and label overlaps. Detectors that reason about measurement keep the narrower reading, because a Grouping carries a measured label width and a native group does not. Title geometry is read from the object, not assumed. The band a title occupies is placed from that object’s own textAlignment and verticalTextAlignment rather than being modelled as centred-and-top, and it is clipped to the object’s own height — Archi clips a figure’s contents to the figure, so a wrapped title on a thin container cannot be reported as extending past its own bottom edge. A container with no usable height has no title band at all. Where a title was never measured — a group’s title, or an element whose label measurement failed — the dimension reports partial instead of certifying a clean result it did not earn. Optional includeViolatorIds returns per-metric visual object IDs for targeted fixes. Optional scope selects single (default — the one viewId) or all-views, which assesses every diagram and returns a compact per-view map ({name, overallRating, overallExcludingAcceptedCosmetics, elementCount, connectionCount, overlapCount, cousinOverlapCount, ownIconOverLabelCount, boundaryViolationCount, parentLabelObscuredCount, nonOrthogonalTerminalCount, crossElementPassThroughCount, contextualPartialDimensions}) for a one-call close-out sweep, then drill into any fair/poor view with a single-scope call. contextualPartialDimensions names the dimensions that view’s run could not fully examine, so every zero beside it on that view is honest yet certifies nothing — it lists only the contextual downgrades, since the permanent partial declarations and the standing not-checked are present on every run and are read from the full coverage map. The key is always present; an empty list means nothing was left unexamined on that view, which is a measured result and not a missing field, so a close-out sweep can tell “nothing to render-verify” from “the sweep did not say” Fan-out sizing preconditions. The response carries an unsizedHubs block naming every element whose connection count is above the fan-out gate and whose box is below the absolute floor for that count (300 + 10 × max(0, count − 7) wide, 250 + 8 × max(0, count − 7) tall), each row carrying the element and view-object ids, the name, the connection count, the box it has and the box it needs. Deliberately the floor and not detect-hub-elements’ growth suggestion, which is relative to the current size and so can never be satisfied. The block answers on a view with zero stored bendpoints, which is where hubPortQualityScore is vacuously 1.0 — so it is the one signal available at the moment sizing a hub is still cheap, before the first auto-route-connections. It feeds no rating: no breakdown entry, no tier, no coverage row. |
detect-hub-elements |
Identify hub elements by counting visual connections per element, sorted descending. Hub thresholds: ≥ 5 connections is a hub candidate; > 6 connections gets an explicit 1D sizing suggestion baseDimension + 15px × (connectionCount − 6); for elements with > 12 connections the response also surfaces a 2D-resize suggestion (width += 15 × ⌈excess/2⌉, height += 15 × ⌊excess/2⌋) so the agent can distribute ports across all four edges. The assessor’s internal M5_FACE_GUARD_MIN_CONNECTIONS = 4 is a separate per-face guard for the M5 hub-port-quality metric, not a hub-detection threshold |
View Operations (1 tool)
| Tool | Description |
|---|---|
auto-connect-view |
Create visual connections for all existing model relationships between elements already placed on a view. Optional showLabel: false to suppress labels, and optional lineColor, fontColor, lineWidth to batch-style every connection it creates (combine with relationshipTypes filter to style by type). Optional relationshipIds is an explicit allow-list for a curated slice — a strict producer → hub → consumer cut that a type filter cannot express — AND-composed with elementIds and relationshipTypes; an unknown id fails fast with RELATIONSHIP_NOT_FOUND before any mutation, while a valid-but-undrawable id keeps the existing silent skip |
Folder Management (5 tools)
| Tool | Description |
|---|---|
get-folders |
List folders (root-level by default, or children of a specific folder) |
get-folder-tree |
Folder hierarchy as a nested tree structure |
create-folder |
Create a new subfolder |
update-folder |
Update folder name, documentation, or properties |
move-to-folder |
Move a model object (element, relationship, view, or folder) to a different parent folder. A move into a folder whose ArchiMate layer is illegal for the object is rejected with FOLDER_LAYER_MISMATCH, the same fail-fast check create-element applies |
Deletion (4 tools)
| Tool | Description |
|---|---|
delete-element |
Delete an element — cascades relationships and view references across all views |
delete-relationship |
Delete a relationship — cascades view connections across all views |
delete-view |
Delete a view and its visual contents (model elements preserved). Reports the cascade it performs: viewConnectionsRemoved (recursive and de-duplicated, so a connection nested inside a group is counted) and viewReferencesRemoved (placeholders pointing at this view, scrubbed from the other views that held them — those placeholders are disconnected, so no dangling connection is left behind in a surviving view) |
delete-folder |
Delete a folder (requires force: true for non-empty folders). A force cascade covers sketch and canvas views as well as ArchiMate diagrams, and folds a contained view’s own external placeholders and connections into the reported totals |
Export (1 tool)
| Tool | Description |
|---|---|
export-view |
Render a view as PNG, JPG, SVG, or PDF — returned inline (base64 / blob) or written to file. Optional quality (1–100, default 90) for JPEG encoding; ignored for other formats. Vector formats (SVG, PDF) leave width/height unset because they are resolution-independent. Optional outputDirectory controls where files are saved (auto-creates directories; defaults to temp). SVG and PDF require the com.archimatetool.export.svg bundle that ships with Archi 5.7+. A raster export is projected to its pixel dimensions and byte estimate before any bitmap is allocated and refused when the machine cannot back it — Archi renders one bitmap covering the whole diagram, so a scale well inside the accepted 0.1–4.0 range can still ask for a buffer larger than free memory, which is a native fault rather than a catchable error. The refusal is an INVALID_PARAMETER naming the projected dimensions, the estimate, the budget, and the largest scale that would have fitted; svg and pdf allocate no bitmap and are never refused this way. Each response carries the model’s monotonic mutation counter as modelVersion (inline mode: top-level field; file mode: in _meta) — compare it numerically against a later assess-layout _meta.modelVersion: a lower export value means the model changed after the render, so the image is stale and should be re-exported before a visual close-out |
Images (2 tools)
| Tool | Description |
|---|---|
add-image-to-model |
Import an image (icon) into the model archive. Preferred: filePath (local file) or url (HTTP download) — these bypass LLM text channel and avoid base64 corruption. Fallback: imageData (base64). Provide exactly ONE per image. Importing a set of icons? Pass an images array instead of calling the tool once per icon — one call imports them all and reports each image’s archive path plus any entry that failed, so a partial batch never needs re-sending whole. Not available inside bulk-mutate: an archive write is not undoable, so it can neither be rolled back nor deferred. Returns the archive imagePath for use with view composition tools. Images are deduplicated |
list-model-images |
List all images stored in the model archive with their paths and dimensions. Use the imagePath values with view composition tools to set images on elements, groups, or notes without re-importing |
Batch & Mutation Control (4 tools)
| Tool | Description |
|---|---|
begin-batch |
Start batch mode — mutations are queued instead of applied immediately. A batch can build on what it has already queued: create a view and add objects to it, create an element and place it on a view, update or re-parent an object the same batch created, and anchor to an object that has not been written yet. Connect two elements the batch just placed, update a connection it just added, embed a view it just created, and lay the whole result out before committing — add-connection-to-view resolves all four of its ids against the queue, apply-view-layout all three of its, and a queued endpoint is scoped to the view the connection lands on. Every parent-fit cascade in the batch is measured against one shared same-batch geometry, so a size queued earlier survives a later pass over the same view; anchors are resolved when the command runs, not when it was queued. A bulk-mutate may be nested inside the batch: it is queued like any other operation, and an id the batch has queued resolves there on every tool that places something on a view — as the view placed on, the parent nested into, and the element placed — and as an update-view-object or update-view-connection target, and in all four of add-connection-to-view’s ids whether or not that operation also uses a back-reference of its own; tools that update, delete or re-file a concept report such an id as not found |
end-batch |
Commit all queued mutations atomically, or rollback (discard all) |
get-batch-status |
Check operational mode and queued operation count. approvalRequired is either true or absent (never false), pendingApprovalCount is absent rather than 0 on an empty queue, and queue fields are absent outside batch mode |
bulk-mutate |
Execute multiple mutations as a single compound command with back-references and optional continueOnError. A back-reference names an earlier operation either by position — $N.id, 0-indexed — or by a name that operation declares with as, written $name.id; a create tool cannot reference its own not-yet-created result. A reference must be the whole value of a top-level params entry — "$0.id" is substituted, "grp-$0.id" is not, and neither is one nested inside an array or object; anything not substituted is passed through as the literal text, which is not an id, so the operation fails naming the $0.id it was handed. Prefer the name in a long call: every position below the current one is a legal earlier operation, so a mistyped position resolves silently into a different, legal container, while a mistyped name matches nothing and is refused — including inside an open batch and while awaiting approval, where no response field reports the container at all. An operation carrying a top-level key other than tool, params and as is refused naming the key, rather than having it discarded in silence. Such a reference to an add-group-to-view or an add-note-to-view works as the viewObjectId of a later update-view-object in the same call, so a group can be created and then re-sized (or renamed via text) and a note created and then re-texted — which re-fits its height — without ending the call. A note is such a target but is never a valid parentViewObjectId: only groups and element view objects can contain anything. Each operation reports effectiveBounds re-read from the model after the write, plus resizedAncestors (containers the operation grew) and movedObjects (anchored children it displaced); an operation on a view connection reports effectiveConnection — the styling, bendpoints, endpoints and anchor points the connection holds afterwards, the same report the single-tool caller gets, so batching work no longer costs the connection facts calling one at a time would have given you; an operation on an ArchiMate concept reports effectiveRelationship or effectiveElement the same way, which is how a bulk caller who sets accessType, associationDirected or influenceStrength — or rewrites an element’s documentation — is told what those became rather than only that the call succeeded; deletions carry the standalone tool’s own cascade counts; and entityName is re-read once the whole call has been applied, so a rename — or a clear to "" — is confirmed by the name the entity ends up with rather than the one it had when the operation was prepared (a delete is the exception, since its id stops resolving the instant it applies). An operation declined by an execute-time integrity guard is reported in skippedOperations with a named reason and flips allSucceeded, rather than coming back as done. A call queued into an open batch or parked awaiting approval reports no allSucceeded verdict at all — nothing has executed, so its operations can still decline at commit — and says so through its batch or proposal sibling instead. Among its operations is the view-scoped set-view-label-expression, which stamps one labelExpression template (e.g. ${name} ${property:evidenceMark}) onto every eligible object on a view in a single atomic op — retroactive, idempotent, and type-filterable (default scope is ArchiMate element objects; optional objectTypes widens to notes/groups). A non-blank-name guard skips a blank-named object when setting (no half-empty glyphs) but is bypassed when clearing; the per-op result reports appliedCount / skippedCount. One sweep = one undo unit |
Undo / Redo (2 tools)
| Tool | Description |
|---|---|
undo |
Undo the most recent mutation operation(s) with optional step count. Scoped to the agent’s own changes: stops at — and refuses to cross — any human edit on the stack, so the agent can never silently revert hand-drawn work (to undo a human change it must submit a new proposal). The human’s native Ctrl+Z is unscoped and still undoes anything |
redo |
Redo previously undone operation(s). Scoped to the agent’s own changes, symmetric to undo |
Approval Workflow (1 tool)
The approval gate is owned by the human in Archi (a desktop toggle), not by the agent. The MCP surface exposes only a read-only observation tool — the agent can see that changes are gated but cannot enable/disable the gate or approve its own queued changes.
| Tool | Description |
|---|---|
list-pending-approvals |
List pending mutation proposals awaiting the human’s approval (read-only) |
Session Management (2 tools)
| Tool | Description |
|---|---|
set-session-filter |
Set persistent filters and field selection that apply to all subsequent queries |
get-session-filters |
Retrieve currently active session-scoped filters and field selection |
Reference & Guidance (1 tool)
| Tool | Description |
|---|---|
get-guidance |
Read the ArchiMate reference material, workflow guides and viewpoint recipes this server ships. Call with no arguments to list every available archimate:// URI; call with uri for the full body. Same content as the MCP resources, reachable by clients that do not expose resource reads to the model (read-only) |
Response Control
All query tools support response optimisation parameters:
- Field selection:
fieldsparam with presets (minimal,standard,full) or custom field arrays - Field exclusion:
excludeparam to omit specific fields - Pagination: Automatic for large result sets, with cursor-based continuation
How far a preset reaches on get-view-contents. A view response carries seven arrays, and fields
routes only two of them — the element and relationship rows — through its verbosity filter.
visualMetadata, connections, groups, notes and images go in whole at every preset, and on a
view carrying connections those five are usually the larger half, so exclude rather than fields
is normally the parameter that moves the response size. Both apply to format: "json" and
format: "graph" only: format: "summary" and format: "tree" return before the field selector
runs, so neither parameter reaches anything there.
MCP Resources
The server provides 14 reference resources accessible to LLM clients.
They are published three ways so that every client can reach them: as static MCP resources, as
the equivalent resource templates (archimate://prompts/{name}, archimate://reference/{name},
archimate://recipes/{name}), and through the get-guidance tool. Clients that do not expose
MCP resource reads to the model can call get-guidance with no arguments to list every URI, or
with a uri to read one. See MCP Resources for detail.
Prompts
| URI | Description |
|---|---|
archimate://prompts/model-exploration-guide |
Strategy guide for LLMs on how to efficiently search and traverse ArchiMate models |
archimate://prompts/explore-dependencies |
Workflow template for systematic dependency analysis of ArchiMate elements |
archimate://prompts/landscape-overview |
Workflow template for generating architecture landscape summaries |
archimate://prompts/routing-preconditions-checklist |
Single-page checklist for LLM agents to fetch before invoking auto-route-connections or auto-layout-and-route on any non-trivial view. Three preconditions (hub sizing, inter-element spacing, group arrangement + corridor-friendly spacing), one verification each, plus a disposition matrix mapping view shape to precondition order |
References
| URI | Description |
|---|---|
archimate://reference/archimate-layers |
Comprehensive mapping of ArchiMate layers to element types with descriptions, plus a concept-to-element-type decision aid (Component vs Service, Process vs Function, Node vs Component, Actor vs Role) |
archimate://reference/archimate-relationships |
All ArchiMate relationship types with valid source/target combinations and usage guidance |
archimate://reference/archimate-specializations |
Specialization (IS-A subtype) vocabulary: when to use specializations vs properties, common patterns per layer, and the discovery/create/audit/delete tool pipeline |
archimate://reference/archimate-view-patterns |
Curated viewpoint patterns, layout algorithm guidance, and diagramming best practices for composing ArchiMate views |
Viewpoint Recipes
A progressive-disclosure recipe library for laying out non-conventional ArchiMate viewpoints. Fetch the index first; it states the invariant build sequence once, routes conventional viewpoints (layered, landscape, hierarchy, information structure) to the view-patterns principles, and points non-conventional viewpoints to a single full recipe page with the topology block to match.
| URI | Description |
|---|---|
archimate://recipes/index |
Selector — fetch first. Conventional-vs-non-conventional decision table, the invariant build sequence, and the family selector |
archimate://recipes/application-integration |
Hub-and-spoke recipe for Application Cooperation / Integration views |
archimate://recipes/behaviour-process-flow |
Swimlane recipe for Business Process Cooperation, plus the Service Design / Customer Journey band layout |
archimate://recipes/motivation |
Directed influence-chain recipe for Motivation views (stakeholder → driver → assessment → goal → requirement) |
archimate://recipes/technology-deployment |
Nested-deployment recipe — nodes as containers with deployed software/artifacts, wired by network paths |
archimate://recipes/roadmap-migration |
Left-to-right plateau-timeline recipe for Implementation & Migration views |
Prompt Library
Separate from the MCP resources above (which an LLM client fetches over the protocol), the top-level prompts/ folder holds ready-to-use prompts you paste into an agent session that has the Archi MCP tools connected. Each file is self-contained: replace any {{PLACEHOLDER}} tokens and run.
| Prompt | Purpose |
|---|---|
prompts/repo-to-archimate-model.md |
Reverse-engineer a code repository into a full, evidence-marked ArchiMate 3.2 model — elements, relationships, folders, and a Context → Landscape → Component stack of views, with every concept marked by evidence strength. Iterative and resumable (the open Archi model is the checkpoint); can target a specific tag, release, branch, or commit. |
prompts/drawio-to-archimate-model.md |
Replicate a draw.io architecture diagram as an ArchiMate model — elements, relationships and a view, with every concept marked by whether it was read from the source or derived by rule. Parses the mxGraph XML directly (no rendering), infers containment from geometry, maps cloud-stencil tokens to (type, specialization) pairs, and reports what it could not resolve rather than inventing it. Two layout modes: semantic (laid out from the ArchiMate view recipes) and mirror (laid out to resemble the source drawing). Takes the diagram’s accompanying design document as an optional second input — which is what turns unlabelled arrows into real relationship types — and vendor icon packages as optional further inputs. The diagram may be a path, pasted mxGraph XML, or an attachment on a Confluence page, and the companion document likewise a path, a pasted body, or that same page; the wiki route documents both draw.io macro generations (the Forge/ADF diagram-name and the legacy diagramName) because a grep for one returns zero on the other, treats a page as a set of diagrams needing an exact filename match, and marks its attachment-download half unverified. A deflate+base64 `` payload is detected before the first trap rather than read as an empty drawing. |
prompts/archimate-model-to-pdf-report.md |
The read-side companion: turn any open Archi model into a print-ready PDF architecture report — a cover page with honest provenance (the model’s own version / date / author properties plus a generation timestamp), an executive summary, every view in an optimal reading sequence with its exported diagram, per-view element tables and notes, then a full element catalogue and relationship register. Optionally embeds the model’s own generating prompt as an appendix. Read-only — query tools only, never mutates. Tool-agnostic (WeasyPrint preferred for page-numbered footers; a Chromium browser works as a fallback). |
archimate-model-to-pdf-report.md also ships with an optional reference implementation, prompts/archimate-model-to-pdf-report.py — a standalone generator that talks to the MCP server over its loopback HTTP endpoint, harvests the model, exports each view and renders the PDF in one command, with --full for complete element and relationship coverage including concepts placed on no view. It needs only the Python standard library plus the weasyprint CLI.
Two of these are also distributed as Claude Code slash commands, each embedding a self-contained copy of its prompt plus an argument-parsing preamble: repo-to-archimate-model.md as /repo-to-archi (adding a repository argument and a clone step), and drawio-to-archimate-model.md as /drawio-to-archi (taking the diagram source — a path, pasted mxGraph XML or a Confluence page — the layout mode, an optional companion document and optional icon packages). The command copies do not auto-update — prompts/ is the source of truth, so re-sync after editing a prompt here. For both commands the sync is mechanical: run tools/sync-drawio-command.sh or tools/sync-repo-command.sh to regenerate a copy, or --check to fail a build when it has drifted; CI gates both. /repo-to-archi additionally rewrites the prompt’s {{REPO_PATH}}/{{REPO_REF}} placeholders into its $ARGUMENTS form, and its generator refuses to emit a copy in which any placeholder survived.
Mutation Safety
All write operations integrate with Archi’s CommandStack, making every mutation undoable via Ctrl+Z in Archi or the undo / redo tools.
Verbatim-store validation. A name, label, view-object text, or labelExpression containing a well-formed HTML/XML entity token (&, <,  , &amp;, …) is rejected with a corrective hint rather than silently stored as the escaped literal — the recurring & footgun. Bare ampersands and non-entity text (R&D, A & B, ``, ${name} & ${property:x}) are accepted and stored byte-for-byte. The rule covers create-* / update-* / add-* and their bulk-mutate equivalents.
Integrity guards hold on deferred paths. A precondition checked when a command is built is worthless once the command is queued, because a sibling operation in the same batch can invalidate it before the command runs. Folder cycles, view-hierarchy and folder-layer placement, and specialization usage are therefore re-checked at execute time, which makes them path-agnostic — the same guard protects the immediate, batch, bulk and approval paths. A guard that trips declines the single operation with a named reason (surfaced in the batch summary and in bulk-mutate’s skippedOperations) instead of proceeding: a folder move that would form a containment cycle, for instance, previously threw nothing and left the affected folders unreachable from the model root. A declined nested folder is also protected from its own ancestor, so an outer cascade cannot remove by containment what an inner decline just saved. The same rule covers the container a create or a placement targets: create-view, create-folder and clone-view resolve a folder when the request is prepared and create into it later, and add-to-view resolves a parent container the same way — so an object whose destination was deleted earlier in the same request is now declined rather than created unreachable from the model root (really written, gone on reload, and reported as a success). A view-nesting parentId belonging to a different view is rejected with it, as are the connections a placement draws for itself: add-to-view with autoConnect and auto-connect-view both scan the view at prepare time, so a preceding remove-from-view could otherwise leave a connection joined to a detached endpoint.
Approval mode adds a human-in-the-loop gate that the human owns — the agent operates tools but can never move the gate or approve its own changes:
Human toggles "Approval Mode" ON in Archi (MCP Server menu)
→ agent's mutations queue as "pending" (not applied)
→ agent calls list-pending-approvals → sees what's gated, tells the user to confirm in Archi
→ human approves/rejects in Archi → apply, or discard
The toggle lives only on the human side: there is no MCP tool to enable/disable approval or to approve a proposal. Your choice is remembered across restarts (stored in MCP preferences); a fresh install defaults to GATED (fail safe). Turning the gate off requires a confirmation in Archi; turning it on is a single click. Approval mode is a single global switch (one human, one desktop, one gate), and the agent can read the current state via get-model-info (approvalMode).
Pending Approvals view. When approval mode is on, you review and apply the agent’s queued changes in the Pending Approvals dock view inside Archi. Open it from Window → Show View → MCP Server → Pending Approvals, or from the MCP Server → Pending approvals (N) menu item (which also shows the live count). Each gated tool-call — including a whole bulk-mutate — is one card, showing a plain-language effect rollup with destructive counts (deletes/removals) called out in amber. Show changes expands the card to named, verb-prefixed rows (deletes hoisted to the top) with a Technical details disclosure for the raw parameters. Approve applies the change (a card containing a delete stays disabled until you expand it once); Reject discards it with no change to the model. Toolbar Approve all safe clears the purely-additive cards in one click, and Approve all ⚠ drains everything after one confirmation that names the deletion count. New gated changes appear live without a refresh.
The card’s effect text is generated by the server from the model’s own truth, so you can decide from the card alone without opening Technical details: relationships name both endpoints (Create ServingRelationship: 'Payment Gateway' → 'Fraud Engine', and a delete adds its cascade), a delete-view or delete-folder card shows the blast radius the agent was given ((cascade: 11 view connections, 1 view reference), (force cascade: 1 view)) rather than the bare target id, and a bulk-mutate card’s rows are named per operation (+ Create "Payment Gateway" (ApplicationComponent), ↔ Connect "Payment Gateway" → "Fraud Engine" (ServingRelationship)). Agents may pass an optional intent on begin-batch or bulk-mutate (a plain-language “why”); when present it shows as a quiet agent's note: line below the effect — it never replaces or outranks the server effect, and an empty or generic note is dropped. Effect is non-spoofable server ground truth; intent is a separate, lower-trust claim the server never depends on.
The card describes the whole write, not part of it. Everything you see on a card — the headline sentence, the expanded rows and the Technical details disclosure — is derived from one map the server builds when the change is proposed, and that map is also what an agent reads back through list-pending-approvals. Two things follow, and both are now enforced rather than maintained by hand. Every parameter the approval will apply is in that map: a tool that accepts a parameter and writes it must disclose it, so you are never authorising an aspect of the change the card kept quiet about. A contract test parses every proposal site in the server and fails the build unless each one is complete or carries a registry line naming the exact parameters it exempts and why. And the card’s own sentence names the aspects that will change, derived from that same map rather than written by hand: renaming a box is announced as a rename, restyling a connection is not announced — or validated — as a reroute, and a call that discloses nothing says so instead of claiming a change it will not make. Under-disclosure hides a write; over-disclosure invents one, and the card is held to both ends.
The gate refuses rather than surprising you. The card is built when the change is proposed, while the command that runs is rebuilt against the current model at approve — so if the blast radius has grown in between (contents dragged into a folder already proposed for a force-delete, say), approval is refused and both numbers are named, instead of deleting five things against a card that said two. A refusal raised while rebuilding is now surfaced with its own reason and remedy (FOLDER_NOT_EMPTY … “Use force: true to cascade-delete all contents”) rather than the generic “a targeted object was changed or removed”, and Approve All reports that same reason when it stops instead of guessing. Cards also say which kind of gate you are looking at: most proposals re-run their preparation against the current model when you click Approve, but fourteen — the layout, routing and bulk-mutate passes — apply the exact compound you reviewed, because re-running a layout can legitimately produce a different result from the one on the card.
Batch mode groups mutations into atomic transactions:
begin-batch()
→ create-element(...)
→ create-relationship(...)
→ add-to-view(...)
end-batch()
→ All succeed together or all roll back
A batch can build on what it has already queued. Inside one begin-batch / end-batch window
you can create a view and add objects to it, create an element and place it on that view, update a
view object the same batch created, use a container the same batch created as a parentId, and
anchor to an object the batch has queued but not yet written — from bulk-mutate running inside the
batch as well as from the standalone tools. Every parent-fit cascade is measured against one shared
same-batch geometry, so two independently-queued cascades can no longer emit competing absolute
resizes of the same grandparent, and a queued size survives a later pass over the same view. Editing
the same object twice in one batch applies only the fields each edit names — and a second edit that
changes only a label or styling no longer discards bendpoints the first edit supplied. Where an id
genuinely cannot be resolved the error names the mechanism — that the object exists but is owned by
the open batch — rather than reporting it as not found.
That covers the whole script an agent runs to draw a view in one unit of work. add-connection-to-view
resolves all four ids it takes (both endpoints, the relationship and the view), so a batch can place
two elements and connect them; apply-view-layout resolves all three of its (the view, each
positioned view object, each connection), so the result can be laid out before the batch ends;
add-view-reference-to-view resolves the referenced view as well as the host; and
update-view-connection can update a connection the same batch added. A queued endpoint is scoped to
the view the connection lands on, so a batch cannot draw a connection whose two ends sit on different
diagrams. Tools that update, delete or re-file a concept still report a queued id as not found — the
standalone call inside the same batch is equally blind, because both callers share one prepare.
Troubleshooting
Server won’t start
- Check if port 18090 is already in use:
lsof -i :18090 - Verify the bind address in preferences is valid
LLM client can’t connect
- Confirm the server is running (menu shows “Stop MCP Server”)
- Verify the port matches your client config
- Test connectivity:
curl -X POST http://127.0.0.1:18090/mcp -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'
Model appears empty
- Ensure an ArchiMate model is open in Archi before connecting
- If the model was opened after the session started, reconnect the LLM client
Mutations fail with validation error
- Check the
archiMateReferencefield in the error — it cites the relevant ArchiMate spec section - Use
search-and-createinstead ofcreate-elementto avoid duplicates
TLS connection issues
- Verify the keystore file exists and the password is correct
- For self-signed certificates, ensure the client trusts the certificate or use
-kwithcurl
Troubleshooting: secure storage (“Secure storage unavailable” when saving the token or keystore password)
The bearer token and the TLS keystore password are stored in Equinox secure storage, encrypted under an OS-protected master password (the macOS Keychain on Mac, the Windows Credential Store / DPAPI on Windows). If that OS-protected master password entry is lost or locked, saving fails with “Secure storage unavailable.” This is an environment/credential-store state issue, not a data problem — the app never falls back to storing the secret in cleartext, and any value already configured is left untouched. (On macOS the underlying error in the log reads SecurityException: Could not obtain password. Result: -25300; on Windows it surfaces as a StorageException reporting the master password could not be obtained.)
To recover, reset secure storage so Equinox can create a fresh master password:
- Quit Archi.
- Move aside the secure-storage file (a backup, so it’s reversible). It lives in the Archi instance area:
- macOS:
mv ~/Library/Application\ Support/Archi/secure_storage \ ~/Library/Application\ Support/Archi/secure_storage.bak - Windows (PowerShell):
Move-Item "$env:APPDATA\Archi\secure_storage" "$env:APPDATA\Archi\secure_storage.bak"
.metadata/.logfile referenced below. - macOS:
- Restart Archi. Equinox recreates the store automatically (you may get a one-time OS prompt to allow access to the Keychain / Credential Store — allow it).
- Re-enter any secrets that lived in the old store: the bearer token (Generate / Regenerate in preferences), the TLS keystore password (re-type it or use Generate Self-Signed Certificate), and — if you use them — any other Archi credentials (e.g. coArchi model-repository logins).
Note: Archi (unlike the full Eclipse IDE) does not expose the Error Log view in its menus on any platform. To read the full stack trace, open the
.metadata/.logfile inside your Archi instance area directly (~/Library/Application Support/Archi/.metadata/.logon macOS,%APPDATA%\Archi\.metadata\.logon Windows).
Developer Guide
The following sections are for developers who want to fork, extend, or contribute to the plugin.
For comprehensive technical documentation covering architecture internals, coordinate model, routing pipeline, and extension patterns, see docs/. Metric acronyms and routing terms used throughout this README (M1–M6, R8, parallelConnectionGap_V_p10, HPQ, corridor, clearance, perimeter) are defined in the glossary.
Project Structure
arch-mcp-server/
├── net.vheerden.archi.mcp/ # Main plugin bundle
│ ├── META-INF/MANIFEST.MF # OSGi bundle configuration
│ ├── plugin.xml # Eclipse extension points
│ ├── src/net/vheerden/archi/mcp/
│ │ ├── McpPlugin.java # Plugin lifecycle & preferences
│ │ ├── server/ # Jetty + MCP SDK wiring
│ │ ├── handlers/ # MCP tool implementations (19 handler classes)
│ │ ├── model/ # EMF model access layer
│ │ │ ├── geometry/ # Geometry utilities
│ │ │ └── routing/ # Connection routing pipeline
│ │ ├── search/ # Full-text search engine
│ │ ├── response/ # Response formatting & DTOs
│ │ ├── registry/ # Tool & resource registries
│ │ ├── session/ # Session management & caching
│ │ ├── logging/ # SLF4J to Eclipse ILog bridge
│ │ └── ui/ # Preferences, menus, startup
│ ├── resources/ # MCP resource content files
│ └── lib/ # Bundled dependencies
│ ├── mcp-sdk/ # MCP Java SDK 0.17.2
│ ├── jetty/ # Jetty 12.0.18 (ee10)
│ ├── jackson/ # Jackson 2.16.1
│ ├── elk/ # Eclipse Layout Kernel 0.11.0
│ └── slf4j/ # SLF4J 2.0.11
├── net.vheerden.archi.mcp.tests/ # Test fragment (OSGi)
└── README.md
Architecture Layers
The codebase enforces strict layer boundaries to keep concerns separated:
┌─────────────────────────────────────────────────────┐
│ Layer 1 — Protocol server/, registry/ │
│ Only MCP SDK + Jetty types. No EMF imports. │
├─────────────────────────────────────────────────────┤
│ Layer 2 — Handlers handlers/ │
│ DTOs + ArchiModelAccessor interface only. │
│ No EMF or SWT imports. │
├─────────────────────────────────────────────────────┤
│ Layer 3 — Model model/ │
│ ONLY package that imports EMF / ArchimateTool. │
│ Returns DTOs, never EObjects. │
├─────────────────────────────────────────────────────┤
│ Layer 4 — UI ui/ │
│ SWT/Eclipse UI only. Preferences, menus, status. │
│ Never blocks Jetty threads. │
└─────────────────────────────────────────────────────┘
Key rule: Handlers never see EMF objects. All model access goes through the ArchiModelAccessor interface, which returns DTOs.
Key Architecture Decisions
Transport
- Dual transport: Streamable-HTTP (
/mcp) + SSE (/sse) for backward compatibility - Each transport gets its own
McpSyncServerinstance HttpServletStreamableServerTransportProviderandHttpServletSseServerTransportProviderboth extendHttpServlet
Threading
- Reads: Direct EMF access from Jetty threads (thread-safe for read-only)
- Mutations: Dispatched to the SWT UI thread via
Display.syncExec()for CommandStack consistency - UI thread is never blocked by read operations
Response Envelope
Every tool response follows a standard structure:
{
"result": { },
"nextSteps": ["Use get-relationships to explore connections", "..."],
"_meta": { "totalCount": 42, "isTruncated": false, "durationMs": 12 }
}
Effective State
A mutating tool’s success response reports what the model now holds, never the values the caller
passed in. This matters because Archi silently auto-fits a container to its children and cascades a
fit up the containment chain, so a requested width / height / x / y may not survive the write
— and the client is an agent that cannot see the canvas, so the response is its only ground truth.
Geometry is re-read after the write (effectiveBounds), a connection’s styling, bendpoints and
anchor points are re-read with it (effectiveConnection), an ArchiMate concept’s own state is
re-read beside them (effectiveRelationship / effectiveElement, which carry the semantic
attributes and documentation a bulk caller can change), an entity’s reported name is re-read once
the whole call has been applied (entityName, so a rename or a clear is confirmed by the value the
model ends up holding), and the collateral is named rather than counted: resizedAncestors and
resizedGroups carry the containers a call grew with their new rectangles, nestedContainersFitted
carries the descendant containers a recursive pass re-fitted, resizedElements carries the leaf
children a layout pass re-sized without descending into them, and movedObjects carries the anchored
children it displaced with their new positions. A count or a flag on its own does not satisfy this —
resizedCount: 3 says something moved and leaves the agent unable to say where — and a field named
as an outcome is never sourced from the request that asked for it. The same rule reaches a count of
zero: layout-within-group’s ancestorsResized: 0 was true and undecodable, so that tool now
also reports ancestorPropagation, a stable code saying which situation produced the number. The
count cannot stand in for it in either direction: only propagated guarantees a count above zero,
five codes always report zero, and two — stopped-at-non-native-ancestor and depth-cap-reached —
arrive with either. Those two are the ones that leave a container above yours too small, so a
positive count there reads as success and is not. A response field that reports an
outcome is also omitted where nothing has run: a bulk-mutate queued into a batch or parked
awaiting approval reports no allSucceeded verdict, because the operations it describes can still
decline at commit.
In batch and approval modes nothing is effective by construction, so the entity is nested
under preview beside a batch or proposal sibling naming the deferred state, and never appears
at the top level of result where an agent would read it as state the model holds.
The rule is enforced, not merely intended: a contract test walks every registered tool and requires each to classify exactly once as read-only, covered by an oracle, or listed in a committed gap registry with a stated reason. A tool classified nowhere fails the build, and the registry’s entry count is a lower-only ceiling — so shipping a tool that cannot report effective state takes a deliberate, reviewable admission rather than silence.
Error Handling
Structured errors with actionable guidance:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Relationship type not valid between these elements",
"details": "ServingRelationship requires ApplicationComponent as source",
"suggestedCorrection": "Use an ApplicationComponent or change relationship type",
"archiMateReference": "ArchiMate 3.2 § 5.1.2"
}
}
The same identifier is deliberately spelled differently across the surface — an element’s model id is
id on update-element, elementId on add-to-view and delete-element, and objectId on
move-to-folder — and no tool schema declares additionalProperties, so a key spelled a sibling
tool’s way is dropped before the handler sees it. A missing-parameter error therefore names the
near miss: it reports the spelling that was supplied alongside the one this tool requires, on the
standalone read and on bulk-mutate’s per-operation read alike, so the caller is not told a
parameter is missing while holding it under a name one synonym away. Inside bulk-mutate the same
failure now carries its own correction rather than the generic “fix the failed operation and retry
the entire call”, which on a 150-operation call is the least useful advice available.
When more than one entry fails, bulk-mutate and apply-positions return a failed array whose rows
carry each entry’s own index, its errorCode, its message and its suggestedCorrection. Rows do
not repeat what they share. A long ending common to every row of one error code is published once
beside the array — in messages and corrections, keyed by that error code — and the row names it
with messageRef or correctionRef. Nothing is truncated: a split row keeps its own head under the
field’s own name, and head followed by the shared string is byte-for-byte what the row used to carry.
A message is never taken away whole, because it is the row’s own account of what went wrong; a row
that shares the whole of its suggestedCorrection carries correctionRef and no
suggestedCorrection at all — absent rather than shortened, so a client that ignores the reference
finds a gap rather than a fragment it would read as complete. Whether a key is present on a row
depends only on that row’s own content, so rows stay independently parseable.
Mutation Pattern
All mutation tools use the PreparedMutation pattern:
- Validate inputs and build the command (on Jetty thread)
- Dispatch execution to UI thread via
Display.syncExec() - Execute through Archi’s CommandStack (enables undo/redo)
- Return result DTO
For connections, redo() must null-then-reconnect due to Archi’s connect() early-return guard.
Adding a New Tool
- Create or extend a handler in
handlers/ - Register the tool in
registerTools()— define the JSON schema, description, and call handler - Add model access if needed — new method on
ArchiModelAccessorinterface, implemented inArchiModelAccessorImpl(returns DTOs, not EObjects) - Format responses using
ResponseFormatterwith the standard envelope - Handle errors — catch at the handler boundary, translate to structured
ErrorResponse - Write tests — mock
ArchiModelAccessorfor handler tests (no EMF runtime needed)
Testing
The test bundle (net.vheerden.archi.mcp.tests) is an OSGi fragment with Fragment-Host: net.vheerden.archi.mcp, giving it full access to main plugin classes.
- Headless one-command run (recommended):
tools/run-tests.shcompiles both projects from source against your local Archi/Eclipse install and runs the headless-safe majority of the suite (~95% of classes), assertingtestsRun > 0per class and emitting JUnit XML tobuild/test-results/. No Eclipse IDE, no hand-maintained class list — the run set is auto-discovered by scanning for*Test.javaminus the exclusion manifesttools/osgi-excluded-tests.txt. Seetools/README.md. Override locations withARCHI_HOME/ECLIPSE_HOME/M2_REPO(so CI can reuse the same script). Usetools/run-tests.sh --swtfor SWT/display-required classes. - Run the OSGi-only bucket in Eclipse as a single “JUnit Plug-in Test” (the
AllPluginTestsRunnerlaunch) — the OSGi runtime it provides is required for the classes the headless harness excludes (listed intools/osgi-excluded-tests.txt). Individual classes can also be run one at a time. - Pure-Java tests (geometry, layout algorithms, routing) can also run as standard JUnit tests without the Eclipse runtime
- Handler tests mock
ArchiModelAccessor— no Archi installation needed - Tests requiring the Eclipse/OSGi runtime — three classes touch Archi/Eclipse runtime singletons that only initialise under OSGi, so they must be run as a JUnit Plug-in Test (a headless JVM cannot initialise them and they are guarded to skip there rather than fail):
McpPreferenceInitializerTest—org.eclipse.core.internal.preferences.ConfigurationPreferencesMcpServerManagerTest—com.archimatetool.editor.model.IEditorModelManagerArchiModelAccessorImplTest(the two specialization-relationship cases) —com.archimatetool.model.util.RelationshipsMatrix
Dependencies
| Library | Version | Purpose |
|---|---|---|
| MCP Java SDK | 0.17.2 | Model Context Protocol implementation |
| Jetty | 12.0.18 (ee10) | Embedded HTTP server |
| Jackson | 2.16.1 | JSON serialization |
| Eclipse Layout Kernel (ELK) | 0.11.0 | Layered graph layout algorithms |
| SLF4J | 2.0.11 | Logging (bridged to Eclipse ILog) |
| Jakarta Servlet API | 6.0.0 | Servlet API for Jetty ee10 |
| Project Reactor | 3.7.0 | Async support (MCP SDK transitive) |
Eclipse/Archi runtime dependencies: org.eclipse.ui, org.eclipse.core.runtime, org.eclipse.swt, org.eclipse.jface, org.eclipse.zest.layouts, com.archimatetool.model, com.archimatetool.editor.
PDE Build Notes
This is a pure Eclipse PDE project — no Maven or Gradle. Key files:
MANIFEST.MF— OSGi bundle metadata,Bundle-ClassPathlists all JARsbuild.properties— PDE build includes.classpath— Eclipse project classpath
When adding a new JAR dependency, update three places: the lib/ directory, MANIFEST.MF Bundle-ClassPath, and .classpath.
Acknowledgments
- Archi - Archi® modelling toolkit
- ArchiMate - ArchiMate® Specification
- Eclipse IDE - Eclipse IDE™
- MCP — Model Context Protocol™
- ELK - Eclipse Layout Kernel™
- Jackson - Jackson JSON Library
- Jetty - Eclipse Jetty® - Web Container & Clients
- SLF4J - Simple Logging Facade for Java
License
This project is licensed under the MIT License. See LICENSE for details.
설치
uvx mcp-proxy --transport streamablehttp http://127.0.0.1:18090/mcp설정
{
"mcpServers": {
"archi": {
"type": "http",
"url": "http://127.0.0.1:18090/mcp"
}
}
}