MCP serverMCP servertypeExposes tools to a coding agent over MCP.See every one →What this type is →by a community orgby a community orgoriginPublished by a community organisation, so it outlives any one maintainer.See every one →

@abap2ui5/mcp-server

An MCP server that puts the abap2UI5 development loop on the machine an agent runs on: check the view the ABAP builds, photograph it, deploy the class into a sandbox, boot the app headless and read the errors.

An agent asked to write an abap2UI5 app can write the ABAP; finding out whether it works has needed a system to activate the class in. This server puts that loop on the local machine: validate_view and screenshot_view reconstruct the UI5 view the builder chain produces and answer with findings and a picture, deploy_app writes the class and its abapGit sidecar into a gitignored sandbox and runs abaplint over it against the framework sources, build_backend transpiles the deployed apps to Node, run_app boots one headless and answers with the page errors and a screenshot, and run_unit_tests runs the apps' ABAP Unit tests in the open-abap runtime. The knowledge tools (app_guide, api_reference, pitfalls, examples, docs_search) read the project's committed files from a checkout, or from GitHub when no checkout resolves; the view gates come from the project's linter, which npm installs beside the server as its declared peer dependency. The app_* tools drive a running app over the abap2UI5 JSON protocol and answer with an agent snapshot of the screen: the fields an agent may fill and the events it may fire, each act checked against that snapshot before it is sent. Getting a finished app onto a real system stays abapGit; with A2UI5_MCP_SYSTEM_URL set the same server offers the app tools against a configured system instead of the sandbox.

Facts

Latestv1.0.0ReleasedLast pushLicenceMIT

Where it sits

Lives in
Provides
Agent access to your codeABAPAgent access to your codecapabilityLetting a coding agent read the project or the system you actually have, reach its model, its objects and its checks, and act on it. In file-based environments that works out of the box; complex systems need dedicated access capabilities. Whatever the environment, the agent also needs project- and framework-specific context, so it understands the thing in front of it rather than guessing from what it remembers of its training data. The transport is incidental: an MCP server, a language server or a CLI answer the same question, and what differs between them is how much of the real thing the agent can see. Distinct from [MCP servers](capability:mcp-servers), which is a protocol adapter on your own running service, and from [agent guidance](capability:agent-guidance), which is knowledge handed over as files rather than read from the thing itself.Unit testingABAPUnit testingcapabilityTesting application logic without a running business process — and the test doubles that make dependencies on the database and on framework services tractable.Agent access to your systemsABAPAgent access to your systemscapabilityGiving an AI agent access to the business data, configuration or operations of systems you run through an MCP server. [Serving your application over MCP](capability:mcp-servers) exposes your own service contract; system access reaches the systems behind it, and one implementation may do both. Development assistance belongs to [agent access to your code](capability:agent-code-access).
Part of
abap2UI5abap2UI5artifactOne ABAP class implementing z2ui5_if_app is the whole app: the browser loads a generic UI5 shell once, and every interaction after that is an HTTP roundtrip back into ABAP.
Requires
abap2UI5abap2UI5artifactThe backend the deployed apps run on: build_backend downloads the released backend of a framework checkout where there is one, and otherwise installs the framework already transpiled from npm and transpiles only the deployed apps against it.
Optional
PlaywrightPlaywrightoutside the atlasThe browser the reconstructed view is rendered in and the app is booted in; A2UI5_MCP_CHROMIUM or a system chromium can stand in for the Playwright-managed one.headless browser

Who is behind it

abap2UI5abap2UI5OrganisationMaintains this
In the atlas: maintains 18
@abap2UI5
oblomov-devoblomov-devPersonMaintains this
In the atlas: maintains 4, contributes to 44
@oblomov-dev
maintainers

Before choosing this

Conditions

PracticalFunctional boundary

Every gate here runs off-system, so what the class does on a real system and what the view does on the oldest UI5 release it has to serve are what the pitfalls catalogues carry and no local run can see. It runs abap2UI5 app classes, so ABAP that is not one has nothing here to deploy into. In system mode the sandbox tools are not offered, and an app_act event runs on the system as the configured user.

PolicySAP API Policy 4.2026a§ 1.1, 1.2, 2.2.2from Agent access to your codeAn MCP server calls ADT or another SAP API of a running system for an agent.
  • § 1.1 Published APIs. Published APIs are made available for the documented purposes identified by SAP, including integration, extensions and data exchange.
  • § 1.2 Non-Published APIs. Customer and third-party applications must not access non-published APIs except where documentation permits it or SAP otherwise authorizes it.
  • § 2.2.2 AI call sequences and data extraction. Outside SAP-endorsed architectures, data services or expressly identified pathways, the policy prohibits specified agentic API use and systematic or large-scale extraction.

An agent that plans and executes ADT calls is the case section 2.2.2 names. Check that the endpoints are published for the intended use, and that the design runs within a pathway SAP identifies for it.

SAP API Policy 4.2026a →
PolicySAP API Policy 4.2026a§ 1.1, 1.2, 2.2.2from Agent access to your systemsAn MCP server uses SAP APIs to supply tools, resources or data to an agent.
  • § 1.1 Published APIs. Published APIs are made available for the documented purposes identified by SAP, including integration, extensions and data exchange.
  • § 1.2 Non-Published APIs. Customer and third-party applications must not access non-published APIs except where documentation permits it or SAP otherwise authorizes it.
  • § 2.2.2 AI call sequences and data extraction. Outside SAP-endorsed architectures, data services or expressly identified pathways, the policy prohibits specified agentic API use and systematic or large-scale extraction.

MCP does not replace the API's documented-use boundary. Check that every endpoint is published for the intended use and whether the policy's AI-call or extraction provisions apply to the design.

SAP API Policy 4.2026a →

What it declares

Every artifact

Type
MCP server
Origin
By a community org
Runtime
Node.js

As an MCP server

Transport
stdio

For agent access to ABAP code

Read
sourcesearchwhere-usedDDICCDStable data
Change
writecreateactivatetransportsgit
Check
syntaxATCABAP Unit
Run
run codedumps
property statedstated nonot said

Tasks this contributes to1Beta

Work on ABAP with an agent and no system connectionA developer wants to review, write or test ABAP with an agent on a machine that has no connection to an SAP system — a serialised repository checked out, or source pasted into the session, and nothing else.Standard ABAP, ABAP CloudCoversthe framework's whole development loop as tools, stated to need no system
What it does

The framework's own MCP server carries the loop as tools: validate_view and screenshot_view rebuild the view the ABAP builds and photograph it, deploy_app writes the class into a gitignored sandbox and runs abaplint over it against the framework sources, build_backend transpiles the deployed apps to Node, run_app boots one headless and answers with the page errors, and run_unit_tests runs the app's ABAP Unit tests in the open-abap runtime. Its documentation states that all of that happens on the machine with no SAP system involved, and the same runner is what the app template's offline test job calls. Beside the gates, the knowledge tools read the guide, the client API and the pitfall catalogues out of the repositories' committed files, so for abap2UI5 the knowledge this record is otherwise about arrives as a tool answer. Nothing else recorded here boots an app.

Remaining work

The loop is the abap2UI5 one: what deploy_app writes is a framework app class, so other ABAP has nothing here to deploy into. A browser is needed for the picture and the headless run, and without a framework checkout the knowledge tools read their files from GitHub, so the machine needs a network connection even where it needs no system. The runner names its own limit: a test that passes here passes on the system short of what the open-abap runtime cannot model, and a PARTIALLY IMPLEMENTED test double has to implement every method the code under test calls. With A2UI5_MCP_SYSTEM_URL set the server's app tools run against a configured system instead, which is the half this record does not assess.

All options on the Agent guidance page →

Connections8

@abap2ui5/mcp-server connections@abap2ui5/mcp-serverPROVIDES→ provides3provides stated Gives a coding agent the off-system loop for an abap2UI5 app class: the view the ABAP builds is reconstructed and checked, the class is written into a gitignored sandbox and linted with abaplint against the framework context, the deployed apps are transpiled to Node and booted headless with the page errors and a screenshot returned, and the app's own ABAP Unit tests run in the open-abap runtime. The framework's guide, its client API and its pitfall catalogues are read live from the repositories instead of recalled. abap2UI5 documentation — MCP Server: https://abap2ui5.github.io/docs/advanced/mcp_server.html abap2UI5 documentation — MCP Server, api_reference: https://abap2ui5.github.io/docs/advanced/mcp_server.htmlAgent access to your codeprovides stated With `A2UI5_MCP_SYSTEM_URL` set, the same server offers only the app tools against a configured SAP system: list app classes from the ADT quick search, start one, read its agent snapshot and fire its events as the configured user over Basic authentication. The sandbox tools are not offered in that mode, and an act runs for real. abap2UI5/mcp-server — README, system mode: https://github.com/abap2ui5/mcp-server abap2UI5/mcp-server — README, system mode, app_act: https://github.com/abap2ui5/mcp-serverAgent access to your systemsprovides stated Runs the ABAP Unit tests of the deployed app classes in the open-abap runtime, and offers the same run outside an agent session as the `abap2ui5-unit` command and a GitHub Action over a repository's own classes. abap2UI5 documentation — MCP Server, run_unit_tests: https://abap2ui5.github.io/docs/advanced/mcp_server.html abap2UI5/mcp-server — README, unit tests in CI: https://github.com/abap2ui5/mcp-serverUnit testingNEEDS→ connects to2connects to stated If: System mode: `A2UI5_MCP_SYSTEM_URL` is set Starting an app on a real system and firing its events over the abap2UI5 JSON protocol at the launch URL, logged on as the configured user; the client does the CSRF token handshake and keeps the stateful session's `sap-contextid`. abap2UI5/mcp-server — README, system mode variables: https://github.com/abap2ui5/mcp-server abap2UI5/mcp-server — README, system mode, protocol client: https://github.com/abap2ui5/mcp-serverabap2UI5ifconnects to stated If: No checkout resolves and no environment variable points at one Reading the repositories' committed knowledge files into a per-user cache kept for a day, and listing the documentation's pages through the GitHub API. abap2UI5 documentation — MCP Server, Level 0: https://abap2ui5.github.io/docs/advanced/mcp_server.html abap2UI5 documentation — MCP Server, Level 0, API limit: https://abap2ui5.github.io/docs/advanced/mcp_server.htmlGitHubif→ needs1needs stated The backend the deployed apps run on: `build_backend` downloads the released backend of a framework checkout where there is one, and otherwise installs the framework already transpiled from npm and transpiles only the deployed apps against it. abap2UI5 documentation — MCP Server, Level 3: https://abap2ui5.github.io/docs/advanced/mcp_server.htmlabap2UI5→ optionally needs1optionally needs stated If: A tool that renders or boots in a browser is used: `screenshot_view`, `run_app` or `interact_app` The browser the reconstructed view is rendered in and the app is booted in; `A2UI5_MCP_CHROMIUM` or a system chromium can stand in for the Playwright-managed one. abap2UI5 documentation — MCP Server, Level 1: https://abap2ui5.github.io/docs/advanced/mcp_server.htmlPlaywrightifLINEAGE→ part of1part of stated abap2UI5 documentation — Tooling: https://abap2ui5.github.io/docs/advanced/tooling.htmlabap2UI5

Install

npm add @abap2ui5/mcp-server

Derived from the package name. No recipe recorded; the README has the configuration.

Where @abap2ui5/mcp-server runsThird partyabap2UI5/mcp-server — README, one-click install, checked 2026-10-17: “Every button registers the same stdio server, named `abap2ui5`”stdiodocumentedoutboundif System mode:`A2UI5_MCP_SYSTEM_URL` is setdocumentedmay needif A tool that renders orboots in a browser is used:`screenshot_view`, `run_app` or …documentedoutboundif No checkout resolves and noenvironment variable points atoneYour MCP client, actorYour MCP clientPlaywright, SoftwarePlaywrightSoftware@abap2ui5/mcp-server, module@abap2ui5/mcp-serverabap2UI5, moduleabap2UI5GitHub, Hosted serviceGitHubHosted service
SAPnot SAP's, or the project's ownstated in the documentunder a condition

README

quoted from abap2UI5/mcp-server under MIT, read
abap2UI5/mcp-server/README.mdOn GitHub

mcp-server

The MCP server for abap2UI5 — gives any AI coding agent (Claude Code, Cursor, VS Code Copilot, or any MCP client) the full abap2UI5 development loop, without an SAP system:

examples -> app_guide -> validate_view + screenshot_view -> deploy_app -> build_backend -> run_app -> pitfalls
(has somebody  (how an app  (SECONDS, no system:        (write ABAP,  (transpile      (boot headless,  (what a green
 built it       is built)    is the view legal,          lint)         to Node)        errors +         run still
 already?)                   and what does it LOOK like)                               SCREENSHOT)      does not prove)

The agent writes an ABAP class, validates the view and looks at a picture of it in seconds, deploys it, boots it in a real browser and looks at the running app — then iterates. Everything runs locally on infrastructure that already guards the abap2UI5 ecosystem in CI: the abaplint transpiler + open-abap runtime, the framework's express shim, the samples-controls build and boot gates, and the linter validation core.

Documentation

→ The MCP server, in full — what MCP means here, the three setup levels and what each one buys, how to register the server with your client, every tool with what the agent gets from it, and the loop they are meant to be used in.

→ Building with AI — the whole AI setup in rising order of effort. This server is the top rung; the cheaper ones matter first.

Quick start

One-click install

Install in VS Code Install in VS Code Insiders Install in Cursor

Every button registers the same stdio server, named abap2ui5:

{"command":"npx","args":["--yes","-p","@abap2ui5/mcp-server","abap2ui5-mcp"]}

The badges go through a web redirect because GitHub does not render links to an editor's own URL scheme; the direct links, to paste into a browser or xdg-open/open, are:

vscode:mcp/install?%7B%22name%22%3A%22abap2ui5%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22--yes%22%2C%22-p%22%2C%22%40abap2ui5%2Fmcp-server%22%2C%22abap2ui5-mcp%22%5D%7D
cursor://anysphere.cursor-deeplink/mcp/install?name=abap2ui5&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyItLXllcyIsIi1wIiwiQGFiYXAydWk1L21jcC1zZXJ2ZXIiLCJhYmFwMnVpNS1tY3AiXX0%3D

(vscode-insiders: in place of vscode: for Insiders.) Claude Code:

claude mcp add abap2ui5 -- npx --yes -p @abap2ui5/mcp-server abap2ui5-mcp

Claude Code plugin. The abap2UI5 plugin bundles the framework's agent skills (building an app, the ABAP and UI5 pitfall catalogues, the view-chain layout, ...) together with this server, registered the same way as above — one install for both:

/plugin marketplace add abap2UI5/abap2UI5
/plugin install abap2ui5@abap2ui5

Releases from 1.0.0 on are also listed in the official MCP Registry as io.github.abap2UI5/mcp-server (server.json), for clients that install from the registry. The same command is what the Smithery (smithery.yaml) and Glama (glama.json) directories list: one npm package, one version, whichever catalogue a client reads.

That is level 1 below: validate_view and fix_view work out of the box, and setup_status tells the agent what else works on this machine and what is missing.

What each install brings

Level 1 — validate_view, fix_view and screenshot_view, the tools most work happens at. They need the linter, which the server declares as a peer dependency (@abap2ui5/linter, >=0.8.0 <0.10.0 — the range this server is built against). npm 7+ installs a non-optional peer by itself, so the one-liner below brings the linter along and validate_view/fix_view work out of the box. The render gate behind screenshot_view needs the UI5 libraries and Playwright on top (@abap2ui5/linter-render, declared as an optional peer: the compatible range is stated, the package is NOT installed for you — plan on ~150–200 MB and a few minutes the first time you add it):

claude mcp add abap2ui5 -- npx --yes -p @abap2ui5/mcp-server abap2ui5-mcp                            # validate_view, fix_view
claude mcp add abap2ui5 -- npx --yes -p @abap2ui5/mcp-server -p @abap2ui5/linter-render abap2ui5-mcp # + screenshot_view

The server looks for the linter, in this order: AI_VIEW_CHECK_HOME; a linter checkout next to the server (which is also where npm puts the peer for an npx run: node_modules/@abap2ui5/{mcp-server,linter}); the project it is started in (node_modules/@abap2ui5/linter — app-template has it as a devDependency, so npm install there is all it takes); the server's own node_modules; whatever Node's module resolution finds from the server's location. A set env var decides alone. Or work from a checkout:

git clone https://github.com/abap2UI5/linter        # AI_VIEW_CHECK_HOME
cd linter && npm ci

The server itself is on npm, so it needs no checkout; the registration above is the whole install. (With npm 6 or --legacy-peer-deps, add -p @abap2ui5/linter yourself.)

(-p … abap2ui5-mcp names the bin explicitly, which every published version answers. The shorter npx --yes @abap2ui5/mcp-server needs a bin named after the package, which 0.2.0 and earlier do not have — npx stops there with "could not determine executable to run".)

(The install is ~47 MB: 19 MB of it a Playwright driver only run_app uses, 2 MB the linter — paid on the first start, cached after. From a checkout instead: git clone https://github.com/abap2UI5/mcp-server && cd mcp-server && npm ci, then claude mcp add abap2ui5 -- node /path/to/mcp-server/server.mjs.)

Cursor, VS Code and Claude Desktop take the standard stdio shape — the documentation has the JSON, and the two further levels (the sample catalogues and deploying, then the headless build-and-boot loop). A tool whose prerequisites are missing answers with a message naming what it needs; the server starts either way.

The abap2UI5 VS Code extension registers this server for you, and adds a second one of its own for the tools that need a real SAP system.

Everything in one command — for a machine (or Codespace) dedicated to the full build-and-boot loop, setup.sh clones the framework, corpus and linter checkouts next to this repo, installs their dependencies and the headless browser (existing checkouts are reused, safe to re-run):

git clone https://github.com/abap2UI5/mcp-server && ./mcp-server/setup.sh

setup.sh --no-corpus clones the framework and the linter only, which is the whole loop for an app of your own since the framework sandbox exists; the corpus is for scope_of, the full build and the locally served UI5.

A Claude Code started inside the checkout picks the server up automatically via the committed .mcp.json; the devcontainer runs the same setup on create.

Level 0 — no checkout at all. The knowledge tools (app_guide, api_reference, pitfalls, capabilities, examples, read_example, docs_search, scaffold_app, generation_rules) read committed files, so when no checkout resolves and no env var is set they read them from GitHub instead: the files land in a per-user cache (<tmp>/abap2ui5-mcp-remote, a day at a time - used only while it is a directory of the user's own, created 0700; A2UI5_MCP_REMOTE_DIR moves it) that the server treats as a read-only checkout; add_agent_setup reads the template from there too, and writes only into the project it is given. And the expensive half runs on the npm package @abap2ui5/node-runtime (below), so npx --yes -p @abap2ui5/mcp-server abap2ui5-mcp in a fresh project covers deploy_app, build_backend, run_app, interact_app, run_unit_tests and verify_app too, with no clone of anything. What still needs a checkout is the corpus' part (scope_of, build_backend mode full, the locally served UI5). A2UI5_MCP_REMOTE=0 or A2UI5_MCP_OFFLINE=1 switches the mirror off. The docs mirror lists its pages through GitHub's API, which allows 60 unauthenticated requests an hour per address; a GITHUB_TOKEN (or GH_TOKEN) in the environment raises that and is sent to api.github.com only.

The backend without a framework checkout: @abap2ui5/node-runtime. The framework is published on npm already transpiled, with the ABAP sources of the same release commit to transpile apps against. With no abap2UI5 checkout (and none configured), build_backend installs that package once per release into ~/.abap2ui5-mcp/runtime/<version> — with the transpiler it names, express, and app-template's abaplint for the lint, at exact versions with a lockfile, --ignore-scripts — fetches open-abap-core at the commit the release was built with, and then transpiles only the deployed apps against it: seconds, never the framework. deploy_app writes into ~/.abap2ui5-mcp/sandbox (and lints with app-template's own config against the package's sources), run_app boots the package's server with the apps registered, run_unit_tests runs the apps' own tests. The release is the registry's latest (asked through npm, cached a day) unless A2UI5_MCP_RUNTIME_VERSION pins one. Measured here with a cold npm cache: the first install about 10 s, open-abap-core about 1 s, then 7–8 s per build and about a second for the unit tests. setup_status shows the release, the workspace and what the next build would do.

With a framework checkout (A2UI5_HOME or a sibling) everything works as before: build_backend downloads the release's prebuilt backend (backend-<version>.tar.gz) into the checkout the first time and re-transpiles the deployed apps from its node/zz_dev afterwards. A2UI5_MCP_BACKEND=npm runs on the package beside a checkout; build_backend mode prebuilt (or A2UI5_MCP_BACKEND=clone) clones the release into ~/.abap2ui5-mcp/abap2UI5 instead — the path this server took without a checkout until the package existed. The full corpus build stays available as mode: "full" for a checkout on an unreleased commit.

Tools

Every tool reads live from a sibling checkout, and each one needs a specific sibling — there is no "optional" repository, only tools you do or do not use. The Needs column says which checkout a tool is dead without: the linter alone carries validate_view, fix_view and screenshot_view (the fast loop, where most iterations happen — and the one dependency npm installs with the server, as its declared peer), the framework checkout carries the guide, the pitfalls and the interface (all three mirrored from GitHub when it is absent), the npm package @abap2ui5/node-runtime carries the backend when no framework checkout is there, and the corpus carries almost everything else. A tool whose checkout is missing answers with the clone command and env var that fix it.

ToolWhat it doesNeeds
setup_statusWhat resolves, what is built, what is missing and how to fix it — one read, call it firstnothing
capabilitiesWhether abap2UI5 can express a UI5 feature at all, from the verified capability mapsamples-controls
app_guideHow to build an app, live from the framework checkoutabap2UI5
api_referenceThe client API (z2ui5_if_client) with its ABAP-Doc: methods, parameters, defaults, the cs_* constantsabap2UI5
scaffold_appThe files a new project starts from, live from app-template; { class: … } renames throughout, sidecar CLSNAME includedapp-template
add_agent_setupSet up an existing project for AI work — what npm create abap2ui5-app -- --agent-setup does: AGENTS.md, the skills, .mcp.json, the allowlist, both gates and the CI job, written into project_dir; never overwrites, merges package.json/.gitignore, follows the STARTING_FOLDERapp-template
examplesSearch the three sample catalogues, verification status and all — answers with a class to read, never a snippet to trustany of samples / samples-controls / samples-stack (or the GitHub mirror)
read_exampleRead the source of a sample an examples hit named — from the checkout, or fetched from GitHubthe sample's repository (or the GitHub mirror)
docs_searchFull-text search over the documentation site's pages: page, heading, snippet and the published URLdocs
generation_rulesThe rulebook for porting a UI5 demo-kit sample into samples-controlssamples-controls
pitfallsThe defects a green run does not catch: { area: "abap" } and { area: "view" }abap2UI5
scope_ofIn/out-of-scope verdict for a UI5 controlsamples-controls + an OpenUI5 checkout
validate_viewThe linter's gates in seconds, judged by your project's own abap2ui5lint.jsonclinter
fix_viewApply the linter's mechanical fixes and get the corrected source back — writes nothinglinter
screenshot_viewSee the view in seconds — no build, no backendlinter
deploy_appWrite the class + abapGit sidecar (+ test include) into the dev sandbox, then abaplint itnothing (the npm backend's ~/.abap2ui5-mcp/sandbox); samples-controls' src/zz_dev or abap2UI5's node/zz_dev when present
read_appRead a deployed dev app's source back, and whether the built backend already carries itthe sandbox (as deploy_app)
build_backendGet the transpiled Node backend: without a checkout @abap2ui5/node-runtime from npm and only the dev apps transpiled (seconds); with one prebuilt downloads the framework's released backend and incremental re-transpiles the dev apps on top; prebuilt/transpile clone the framework when asked to; full runs the corpus' e2e-buildnothing (npm), or abap2UI5 (full needs samples-controls)
build_logPage through the last build's full output — the error the result's short tail cut offnothing (reads the record the last build left)
run_appBoot an app headless: status, real page errors, and a screenshota build (npm backend or abap2UI5 checkout); samples-controls serves UI5 locally when present, the CDN otherwise
interact_appBoot an app, then click, fill, press and wait through a short script — the event branch, photographeda build (same as run_app)
app_listThe app classes the built backend can start — the deployed dev apps and the framework's owna build (npm backend or abap2UI5 checkout)
app_startStart an app and get its agent snapshot: fields (path, label, kind, value), actions (event + arguments), tables, messages — over the abap2UI5 JSON protocol, no browser; in an MCP Apps host also an interactive screen, with format: "adaptive-card" also an Adaptive Carda build (same as run_app)
app_describeThe current agent snapshot of a session, from memory — no roundtripa session from app_start
app_actFill fields and fire an event by name — validated against the snapshot, sent as the real model delta — and get the next snapshota session from app_start
run_unit_testsRun the deployed apps' test classes (on a checkout: or the whole transpiled tree) in the open-abap runtime: assertions, not picturesa build (npm backend or abap2UI5 checkout)
verify_appThe whole loop in one call — validate, deploy, build, unit, boot — stopping at the first stage that failswhat the stages need
migrate_reportConvert a classic ABAP report into an abap-cloud-gui app class (report2cloud): the class files, the migration report (TODOs, unreleased tables with successors), the refusals with file:row:col; deploy: true builds it here and answers its selection screen's agent snapshotabap-cloud-gui (with npm ci); deploy also the popups and a build
backendstatus / start / stop / restart of the local express backenda build (start/restart; status and stop always work)
remove_appDelete a dev app from the sandbox, or list the deployed onesthe sandbox (as deploy_app)

verify_app is the loop in one call: validate, deploy, build, unit tests and boot, stopping at the first stage that fails and reporting every stage before it. interact_app is run_app with hands: after the boot it clicks, fills and presses through a short script and photographs the result, which is how the event branch of an app becomes visible without a system; run_unit_tests runs the test classes deploy_app wrote beside the app (a local z2ui5_if_client double, see the app guide's chapter 9) in the open-abap runtime and answers with assertions.

scaffold_app and add_agent_setup both execute app-template's own template.json, for the two kinds of project. scaffold_app hands back a whole NEW project, app class included, and writes nothing. add_agent_setup is for the abapGit repository that already exists - most of them never started from the template - and does what npm create abap2ui5-app -- --agent-setup does there, by the template's agentSetup key: it writes the agent setup and the two gates into project_dir (default: the server's working directory, when that looks like a project), never overwrites a file the project has (each one is listed as skipped), merges package.json and .gitignore by only adding what is missing (each value kept that differs from the template's is a warning), points the configs at the source folder .abapgit.xml names and writes nothing into it. A second run changes nothing; dry_run: true answers the same report without writing. Every path it writes comes from the template's file list and is checked first - a plain relative path, a shared file of the template, outside the source folder; one that fails refuses the whole call before anything is written. A symbolic link, or a folder that leads out of the project through one, is never written through: that file is skipped, and the answer says why.

Every answer fits the client. A client shows the agent nothing of a tool result over its cap (Claude Code: 25,000 tokens), so no answer is larger than about 60,000 characters. The tools whose answers grow with upstream content page instead of cutting: matches counts every hit, returned the page, and more names the exact call for the rest (offset - examples, capabilities, api_reference, app_guide, pitfalls, docs_search; files - scaffold_app; from_line - read_app, read_example; build_log names its next offset under cut). A list that is shortened rather than paged says so in a ...Cut field (findingsCut, rulesCut, errorsCut, ...), and a last-line backstop trims anything that still passes the cap and marks it (__answerGuardCut). The resources follow the same budget (below).

examples degrades per catalogue instead of failing: it searches the checkouts it finds and names the ones it could not, so a thinner answer never reads as "nobody has built this". It reads each repository's committed catalogue.json where the checkout has one — which is what carries a control port's verification status (checked over reviewed over generated, used to break ranking ties), the learning-path stage, and what a stack sample needs from the system — and falls back to parsing SAMPLES.md on a checkout from before that file existed. Where samples and samples-controls commit catalogue-derived.json beside it, a query also matches the control types each sample's view builds, so sap.m.Dialog finds every sample that builds one, not only those filed under it. screenshot_view and run_app answer the same question at three orders of magnitude apart: the first photographs the reconstructed view with no backend, the second the running app after a build. Most iterations should end at the first.

Operating an app without a browser

app_start, app_act and app_describe make every abap2UI5 app agent-operable: they speak the JSON protocol the UI5 frontend speaks — the app start, the event with its arguments, the model delta of what was typed — against the local backend, and answer with an agent snapshot derived from the response's view XML and model: the fields an agent may fill (model path, label, kind, current value, choice values, editable), the actions it may fire (event name, static and row-dependent arguments), the tables (columns, the first rows, selection - a SelectDialog/TableSelectDialog is a table too, and app_act with row picks from it), the messages (toast, message box, MessageStrip, field value states, MessagePopover and MessageView items) and some static text. An act is validated against the snapshot before anything is sent: an unknown event, a field that is not on the screen or not editable, a choice outside its values is refused with the list of what is allowed. A short session against z2ui5_cl_smp_app_009 of abap2UI5/samples:

app_start { app: "z2ui5_cl_smp_app_009" }
  -> fields f1..f5 (f3 "Input with value", /S_SCREEN/COLOR_02, text, ""),
     actions a1 POPUP_TABLE_VALUE (valueHelpRequest of f3), ..., a5 BUTTON_SEND
app_act { session, values: { f4: "Smith" }, event: "POPUP_TABLE_VALUE" }
  -> layer "popup", table t1 /T_SUGGESTION_SEL (6 rows, Single, editableCells [SELKZ]),
     action a1 POPUP_TABLE_VALUE_CONTINUE
app_act { session, values: { "/T_SUGGESTION_SEL/2/SELKZ": true }, event: "POPUP_TABLE_VALUE_CONTINUE" }
  -> layer "main", f3 = "BLACK", f4 = "Smith", message { toast, "value selected" }

The snapshot shape is shared with the VS Code extension and the ABAP agent addon; docs/agent-snapshot.md is its reference — the derivation rules, the operations, the deviations from the original contract and what the snapshot cannot see yet. interact_app stays the tool for what only a browser shows (the rendered page, client-side behaviour).

The screen in the chat: MCP Apps and Adaptive Cards

The snapshot is text for the agent. Two optional surfaces show the same screen to the user in the chat, and send what the user does back through app_act - the same validation as the agent's own acts, so nothing the snapshot does not allow can be fired, and the agent sees the act:

  • MCP Apps (SEP-1865, stable 2026-01-26, extension io.modelcontextprotocol/ui). app_start, app_describe and app_act name the UI resource ui://abap2ui5/app-screen (_meta.ui.resourceUri, MIME type text/html;profile=mcp-app) when the client advertises the extension in its initialize. A host that renders MCP Apps (the extension's own README lists Claude, ChatGPT, VS Code and Goose) shows it as a sandboxed iframe beside the result: fields to fill, buttons, row actions, table cells, messages. A click is a tools/call of app_act through the host, built from the snapshot on screen; afterwards the page tells the model what happened (ui/update-model-context) and renders the new snapshot. The page is one self-contained HTML document - no external URL, no network, no eval - so it runs under the spec's restrictive default CSP; it needs nothing else. A2UI5_MCP_UI=on declares the screen for a host that renders MCP Apps without advertising the extension, off never; the tools' text answers do not change either way.
  • Adaptive Cards for Copilot/Teams-style hosts: format: "adaptive-card" on app_start, app_describe and app_act (or A2UI5_MCP_APP_FORMAT=adaptive-card for every call) adds the screen as an Adaptive Card 1.5 - an embedded resource of type application/vnd.microsoft.card.adaptive after the unchanged snapshot - rendered by abap2UI5/protocol's renderer (vendored under lib/vendor/adaptive-cards). Off by default. Each Action.Submit carries session (and row for a row's action); a submitted payload maps to one app_act call: event and row as they are, every input whose value changed as values["<binding path>"], a message box button as the box's close action with the button as its $action argument, the popup/popover close wire as @CLOSE_POPUP / @CLOSE_POPOVER (cardSubmitToAct in lib/adaptive-card.mjs; the full table is in docs/agent-snapshot.md).
Migrating a classic report

migrate_report takes the source of a classic report (REPORT, PARAMETERS, SELECT-OPTIONS, the event blocks, a WRITE list, an ALV) and its .prog.xml text pool, and answers the class of the abap-cloud-gui addon that keeps its programming model — selection_screen( ), start_of_selection( ), write( ), alv( ), message( ) — together with the migration report (the TODOs, the tables and APIs not released on ABAP Cloud with their successors, what the list does not carry over) and the statements it refused with file:row:col (dynpros, batch input, SUBMIT, native SQL). A report that carries open-abap's @KERNEL escape (WRITE '@KERNEL ...'., JavaScript once transpiled) is refused before anything is converted, and a dynamic WHERE (cond) is converted but flagged under warnings - the runtime evaluates it with eval() (SECURITY.md). The converter is the addon's own report2cloud, run from a checkout of the addon — it is not on npm:

git clone https://github.com/abap2UI5-addons/abap-cloud-gui   # ABAP_CLOUD_GUI_HOME
cd abap-cloud-gui && npm ci

With deploy: true the class is also written into the dev sandbox together with the addon's runtime (src/01, its database tables included - the backend creates a deployed table at boot) and the popups it calls (a checkout of popups at POPUPS_HOME, else the addon's .deps/popups, ../popups beside it or the addon's build/popups, in that order), the backend is built, and the answer carries app_start's snapshot of the selection screen — app_act with CGUI_EXECUTE runs the report. The database tables a report reads are not in the local backend: the screen runs, a run that reads them does not.

System mode: operating apps on a real SAP system

Everything above runs without a system. With A2UI5_MCP_SYSTEM_URL set, the same server is something else: the app tools against a real SAP system, logged on as the configured user — for MCP clients that are not the VS Code extension (whose own system server does this for the clients of its window): Claude Desktop, Claude Code, Cursor, any stdio client. The sandbox tools are not offered in this mode; register the server twice, once with and once without the variable, to have both.

ToolWhat it does
system_statusThe endpoint and user (never the password) and ONE request that shows whether the host answers, the certificate is accepted and the logon works — ok only when the abap2UI5 start page answers, not a form logon page that also answers 200 — call it first
app_listClass names on the system, from the ADT quick search (filter: start of the name, * as wildcard; at most 50)
app_startStart an app on the system and get its agent snapshot — the same snapshot, arguments and refusals as the sandbox's app_start
app_describeThe current snapshot of a session, from memory — nothing is sent
app_actFill fields and fire an event, validated against the snapshot, sent as the real model delta — runs for real

Claude Desktop (claude_desktop_config.json: Settings → Developer → Edit Config), the sandbox server beside the system one:

{
  "mcpServers": {
    "abap2ui5": {
      "command": "npx",
      "args": ["--yes", "-p", "@abap2ui5/mcp-server", "abap2ui5-mcp"]
    },
    "abap2ui5-dev-system": {
      "command": "npx",
      "args": ["--yes", "-p", "@abap2ui5/mcp-server", "abap2ui5-mcp"],
      "env": {
        "A2UI5_MCP_SYSTEM_URL": "https://host:44300/sap/bc/z2ui5?app_start={class}&sap-client=100",
        "A2UI5_MCP_SYSTEM_USER": "DEVELOPER",
        "A2UI5_MCP_SYSTEM_PASSWORD_CMD": "security find-generic-password -s abap2ui5-dev -w"
      }
    }
  }
}

Claude Code, the same as one command:

claude mcp add abap2ui5-dev-system \
  -e A2UI5_MCP_SYSTEM_URL='https://host:44300/sap/bc/z2ui5?app_start={class}&sap-client=100' \
  -e A2UI5_MCP_SYSTEM_USER=DEVELOPER \
  -e A2UI5_MCP_SYSTEM_PASSWORD_CMD='security find-generic-password -s abap2ui5-dev -w' \
  -- npx --yes -p @abap2ui5/mcp-server abap2ui5-mcp
Variable
A2UI5_MCP_SYSTEM_URLThe launch URL as the VS Code extension's F9 knows it, {class} as a query parameter — or the endpoint without the class. Switches the mode on
A2UI5_MCP_SYSTEM_USERThe SAP user (Basic authentication)
A2UI5_MCP_SYSTEM_PASSWORDIts password — in the client's configuration file, in plain text
A2UI5_MCP_SYSTEM_PASSWORD_CMDInstead: a shell command that prints the password, run once and kept in memory. macOS keychain: store it with security add-generic-password -s abap2ui5-dev -a DEVELOPER -w, read it as above; Linux: secret-tool lookup service abap2ui5-dev
A2UI5_MCP_SYSTEM_INSECURE_TLS1 accepts a certificate that cannot be verified (a development system's self-signed one). Better: NODE_EXTRA_CA_CERTS pointing at the system's CA certificate (PEM)

What to know before switching it on:

  • It acts for real. An app_act event runs on the system as the configured user and may save, post or delete data. Use a development system and a user whose authorizations fit what the agent may do; the agent sees only what that user may see.
  • A rejected logon is sent once. SAP locks a user after a few failed logons, and an agent retries. After a 401 nothing more is sent with that password — every tool says so — until the server is restarted with the corrected one (Claude Desktop: quit and reopen), or the password command answers a different one.
  • Basic authentication only. SAML, X.509 or SSO logons are not supported in this mode; the VS Code extension's system server (its auth proxy) is the way there.
  • One system per registration. Two systems are two entries with two names.
  • No proxy support. The requests go straight to the host; HTTPS_PROXY is not read.
  • The protocol client is the sandbox's (lib/appclient.mjs): it does the CSRF token handshake and keeps the stateful session's sap-contextid itself; this mode adds the logon, the system's cookies and the start location (lib/system.mjs). app_list needs the ADT services and the user's authorization for them — app_start works without both when the class name is known.

Unit tests in CI, without a system

The same runtime runs an app repository's ABAP Unit tests in GitHub Actions (or at a terminal): @abap2ui5/node-runtime at the release the project's abaplint.jsonc pins, installed once and cached, the classes transpiled against it, the tests run through the generated runner — no framework clone, none of the framework's devDependencies, and no npm ci of this package either (the runner imports none of its dependencies). Against app-template's starter app, measured on one machine: 16 s cold and 8 s with the cache, where the clone of 0.2.0 took 30 s and 23 s and left a 231 MB workspace instead of 67 MB.

- uses: abap2UI5/mcp-server@v1
  with:
    paths: src

framework: X.Y.Z pins another release than the project's, backend: clone takes the old path (the release cloned, its backend downloaded or built) — which is also what a pin older than the package (1.145.0) gets by itself.

@v1 is a floating major tag: the release workflow moves it to every 1.x release it publishes, so a workflow naming it picks up fixes without picking up a breaking change. It is created by the 1.0.0 release; until that release is out, pin @v0 (0.3.0, where it stays) or a release tag such as @v0.3.0.

npx -p @abap2ui5/mcp-server abap2ui5-unit src     # the same, locally

Every class and interface under paths is deployed with all of its files (an app or not; test and local-class includes too), and every test include runs. On the package the run uses a sandbox and a build of its own, so an MCP session's apps on the same machine are neither part of it nor touched by it. The result is the job's verdict plus a step summary naming every test method and the first failure; an object that cannot be deployed (a namespaced name) fails the run rather than leaving its tests out. app-template ships the job in its check.yml and the command as npm run test:unit. What the runner cannot see is what the open-abap runtime cannot model (see pitfalls, area abap); a test that passes here passes on the system short of that, and a PARTIALLY IMPLEMENTED test double has to implement every method the code under test calls, because the runtime generates no empty stubs.

Resources

The knowledge documents behind those tools are also MCP resources, for clients that surface them (context pickers, attach-a-document UIs) and for agents that want a document whole instead of sliced. Same live reads from the same sibling checkouts: listing is free (no checkout needed), reading a resource whose checkout is missing answers with the same actionable error the tool gives. A document longer than a client shows of one read (about 60,000 characters - today the abap-check catalogue and the capability map) is cut at a section heading, and its last line names the tool call that reads on from the first section left out (pitfalls { area, offset }, app_guide { offset }, capabilities { status }). The one resource that is not a document is ui://abap2ui5/app-screen, the MCP Apps screen of the app tools (see "The screen in the chat" above); it is part of this server and needs no checkout.

ResourceContentNeeds
abap2ui5://guideThe app-building guide, whole (app_guide slices it)abap2UI5
abap2ui5://guide/{chapter}One guide chapter, by number or heading keyword (a resource template)abap2UI5
abap2ui5://apiThe client API summary — every z2ui5_if_client method, constant group and type, one line eachabap2UI5
abap2ui5://pitfalls/abapabap-check — the ABAP defects a green CI does not catchabap2UI5
abap2ui5://pitfalls/viewui5-check — the view defects a green CI does not catchabap2UI5
abap2ui5://capabilitiesCAPABILITIES.md — the verified capability mapsamples-controls
abap2ui5://generation-rulesThe rulebook for porting a UI5 demo-kit samplesamples-controls
ui://abap2ui5/app-screenThe MCP Apps screen of the app tools (text/html;profile=mcp-app) - the snapshot as a page the user operates in the chat-

Prompts

Two prompts — one per job this server serves — put an agent straight into the loop instead of leaving it to reconstruct the order from the tool descriptions alone. Each renders an orchestration script over the tools above and duplicates none of their content:

  • build-an-abap2ui5-app (argument: task, what the app should do) — orient with examples/capabilities, learn the shape from app_guide, write the class, iterate through validate_view/screenshot_view in seconds, prove it with deploy_app → build_backend → run_app, close with pitfalls.
  • port-a-ui5-sample (argument: sample, the demo-kit sample) — the corpus job: generation_rules as the brief, scope_of and capabilities before writing, neighbouring ports from examples, then the same validate/screenshot/deploy/run loop.

Notes

  • Dev sandbox: deployed apps land in the samples-controls checkout's gitignored src/zz_dev/, else the abap2UI5 checkout's gitignored node/zz_dev/, else ~/.abap2ui5-mcp/sandbox (A2UI5_MCP_WORKSPACE moves it) — nothing an agent deploys can leak into a commit.
  • Backend: without a framework checkout the npm package @abap2ui5/node-runtime — A2UI5_MCP_RUNTIME_VERSION pins its release (default: the registry's latest, asked once a day), A2UI5_MCP_BACKEND=npm uses it beside a checkout too, A2UI5_MCP_BACKEND=clone restores the framework clone as the default. Everything it installs lives in ~/.abap2ui5-mcp and is safe to delete.
  • Port: the backend listens on 3000 (A2UI5_MCP_PORT overrides).
  • The lint: deploy_app lints with app-template's abaplint config. On the npm backend a long-lived worker keeps @abaplint/core (installed beside @abaplint/cli at the same version) with the framework parsed, so a deploy's lint takes milliseconds instead of a fresh abaplint process (2-3 s, mostly the framework's parse); whenever the worker cannot stand in for the CLI - a checkout without that core, a config it does not mirror - the CLI runs as before. A2UI5_MCP_LINT_WORKER=0 switches the worker off; A2UI5_MCP_LINT_IDLE_MS (default 10 minutes, 0 = never) is how long it is kept without a request - with the framework parsed it holds about 300 MB - before it exits and the next lint starts a fresh one.
  • Timeouts: every spawned child is killed (whole process tree) when it exceeds its limit — lint/scope 5 min, unit tests 10 min, build (with the prebuilt download and the npm install) 30 min by default; A2UI5_MCP_LINT_TIMEOUT_MS, A2UI5_MCP_SCOPE_TIMEOUT_MS, A2UI5_MCP_UNIT_TIMEOUT_MS and A2UI5_MCP_BUILD_TIMEOUT_MS override (values in ms).
  • Download limits: what is read from GitHub is capped by size as well as by time - 8 MB for a file of the read-only mirror, the docs tree listing and the release list (the largest today is 0.34 MB), 200 MB for the prebuilt backend archive (2.7 MB for 1.146.0). An answer over the cap is refused (when it declares its size) or cut at the cap, and the tool says so by URL; a cached mirror or the previous build stays in place.
  • The prebuilt backend is verified before it is unpacked: the framework attaches a backend-<version>.tar.gz.sha256 sidecar beside the archive (releases after 1.148.0), and build_backend compares the download's sha256 against it - a mismatch discards the download and keeps the previous build; a release without the sidecar unpacks as before and the build log says it was unverified. The archive is code run_app boots, so A2UI5_MCP_PREBUILT_URL must be https: - a plain http: mirror off the loopback is refused unless A2UI5_MCP_PREBUILT_INSECURE=1 says the network is trusted (the sidecar is looked for at <url>.sha256 there too).
  • UI5 sources are served from the samples-controls checkout's @openui5 packages when that checkout is there, so booting needs no network; without it run_app and interact_app load UI5 from the CDN (and say so at once when it cannot be reached, instead of waiting out the boot timeout). The built theme CSS is not in those packages — with network access it loads from the CDN (styled screenshots); without, apps render unstyled but structurally complete. What the CDN answers is kept in memory across calls (A2UI5_MCP_UI5_CACHE_MB, default 64; 0 switches it off), so a second boot does not download UI5 again. A2UI5_MCP_OFFLINE=1 forces the hermetic behaviour.
  • Chromium: A2UI5_MCP_CHROMIUM (or CHROMIUM_BIN, which the linter reads too) names the executable; otherwise the Playwright-managed browser (npx playwright install chromium); otherwise a system chromium. setup_status says which one it found and where it came from. validate_view's warm render gate keeps 2 pages of that browser, which concurrent calls queue on; A2UI5_MCP_RENDER_PAGES (1 to 8) sets it.
  • Screenshots: run_app and interact_app write their PNG to <tmp>/abap2ui5-mcp-screenshots/<class>-<stamp>.png (the newest 50 are kept, older ones of that pattern removed) and return the path beside the image — deliberately not into the install directory, which is inside node_modules when you install from npm. The last build's log, which build_log reads after a restart, lives there too. That directory is created 0700 and used only while it is your own (/tmp is shared on Linux); otherwise the image is returned but not saved, and screenshotNotSaved says why. A2UI5_MCP_SCREENSHOT_DIR puts them somewhere you keep.
  • scope_of needs an OpenUI5 checkout as well as the corpus: it reads the JSDoc from OPENUI5_SRC, or from ../fork-openui5 beside the samples-controls checkout when that variable is unset.
  • If you set this up earlier: the corpus repository was ai-demokit, then abap2UI5-api, and is samples-controls today. Nothing needs changing — an existing checkout is still found under any of the three directory names, and AI_DEMOKIT_HOME is still read alongside SAMPLES_CONTROLS_HOME.
  • Real-system deployment stays what it is today: abapGit. This server is the inner dev loop; the real-system half lives in the VS Code extension, whose own MCP server exposes it as run_app_on_system. Both servers are registered in the same editor window, which is why that tool is not called run_app.

Working on this repository

npm ci
npm test

AGENTS.md carries the conventions, CONTRIBUTING.md and RELEASING.md the rest of the workflow.

bench/ is abap2UI5-bench: how often an AI agent's abap2UI5 app passes the template's gates, with and without this server. It is a package of its own and not part of the published server.

MIT, read

History

Latest

v1.0.0

Other 156

Notes

1.0 is a stability promise, not a feature release. This server's tool names and result shapes are a contract with every agent configuration that registers it (RELEASING.md, "What a release is for"); from 1.0.0 on that contract follows semantic versioning, so an unpinned npx registration - the form the README, the VS Code extension and the app template all use - does not break an agent setup between two 1.x releases:

  • Major (2.0.0): a tool, resource URI, prompt, bin or GitHub Action input removed or renamed; a tool argument removed, renamed, retyped or made required; a field of a tool result removed, renamed or retyped, or an answer that was a JSON object no longer being one; an environment variable removed or renamed; a Node.js floor raised.
  • Minor: a new tool, resource, prompt, optional argument, result field or environment variable; a new backend or mode; the linter peer range moving to the linter's next minor.
  • Patch: fixes, and the wording of tool descriptions, hints and messages.

What the promise does NOT cover, because this server bundles no content: the documents it serves (the guide, the client interface, the pitfall catalogues, the capability map, the sample catalogues, the docs) are read live from the abap2UI5 repositories and change when they do, and the backend runs the @abap2ui5/node-runtime release the npm registry names unless A2UI5_MCP_RUNTIME_VERSION pins one. Human-readable text - a description, a degradation message, a hint - is for the agent to read, not to parse.

What 1.0 contains:

  • 30 tools. setup_status (what works on this machine right now, and why not); the knowledge half - capabilities, examples, read_example, app_guide, api_reference, generation_rules, scaffold_app, docs_search, pitfalls, scope_of; the project's own setup - add_agent_setup; the cheap view half, seconds and no backend - validate_view, fix_view, screenshot_view; the dev sandbox - deploy_app, read_app, remove_app; the expensive half on a real transpiled backend - build_backend, build_log, backend, run_app, interact_app, run_unit_tests, and verify_app, the whole loop in one call; operating a running app without a browser - app_list, app_start, app_describe, app_act; and migrate_report, a classic ABAP report as an abap2UI5 app class.

  • Six resources (abap2ui5://guide, abap2ui5://api, abap2ui5://capabilities, abap2ui5://generation-rules, abap2ui5://pitfalls/abap, abap2ui5://pitfalls/view) plus the per-chapter template abap2ui5://guide/{chapter}, the MCP Apps screen ui://abap2ui5/app-screen the app tools render into, and two prompts, build-an-abap2ui5-app and port-a-ui5-sample.

  • No checkout needed: the knowledge tools fall back to a read-only GitHub mirror, and the expensive half runs on @abap2ui5/node-runtime without a framework clone; a sibling checkout or a set *_HOME variable still wins, and a misconfigured one is reported, never worked around.

  • Three bins - mcp-server, abap2ui5-mcp, abap2ui5-unit - and the composite GitHub Action abap2UI5/mcp-server@v1, which runs an app repository's ABAP Unit tests without an SAP system. The release workflow moves the floating major tag to v1 with this release; v0 stays at 0.3.0, so a workflow pinned to @v0 keeps working and moves to @v1 when its owner decides to.

  • Listed in the official MCP Registry as io.github.abap2UI5/mcp-server. server.json describes the npm package (stdio, npx) and every environment variable the server reads - all optional, none with a default, because a set variable is authoritative here. package.json carries the mcpName the registry checks npm ownership by, and the release workflow publishes the listing after the npm publish, in a job of its own, logged in with GitHub OIDC (no secret) through a pinned, checksum-verified mcp-publisher. npm run check:server-json (CI, the release workflow and npm test) fails when server.json's versions, package name or registry name drift from package.json, or when its variable list and the code disagree in either direction; npm version keeps the versions in step by itself (the new version script).

  • One-click install in the README: VS Code and VS Code Insiders badges, a Cursor badge and the direct vscode: / cursor:// links beside the claude mcp add line - all registering npx --yes -p @abap2ui5/mcp-server abap2ui5-mcp, which test/install-links.test.mjs decodes from every link and holds against the registration the README shows in clear - and the Claude Code plugin (/plugin marketplace add abap2UI5/abap2UI5, /plugin install abap2ui5@abap2ui5), which bundles the framework's agent skills with this server.

  • The linter is a declared peer dependency, and npx -p @abap2ui5/mcp-server brings it along. The server had no dependency on @abap2ui5/linter at all: the registration every document gives installed a server whose validate_view, fix_view and screenshot_view were dead unless the user also typed -p @abap2ui5/linter -p @abap2ui5/linter-render (the VS Code extension 0.30.1 grew a warning for exactly this). package.json now names @abap2ui5/linter as a regular peer at >=0.8.0 <0.9.0 - npm 7+ installs a non-optional peer by itself, for npm install and for npx -p alike, hoisted beside the server where the sibling lookup already finds it, so the plain one-liner carries the property gate (1.9 MB) - and @abap2ui5/linter-render as an optional peer in the same range: the compatible range is declared, the 123 MB of UI5 behind the render gate is not installed for you (npm never installs an optional peer); add -p @abap2ui5/linter-render for screenshot_view. The resolver gained a last candidate after the checkout siblings, the project's and the server's own node_modules: the @abap2ui5/linter Node's own module resolution finds from the server's location, for a hoist the explicit paths cannot name. test/view-check-install.test.mjs pins the npx layout, a nested install, that deeper hoist and the project's own devDependency against copies of lib/. The missing-linter hints and the README say what npm installs and what it only declares.

  • add_agent_setup: an existing project set up for AI work. What npm create abap2ui5-app -- --agent-setup does, for any MCP client's agent: app-template's template.json agentSetup key, executed over the project directory the agent names (project_dir; the server's working directory by default, when it has a .git, .abapgit.xml or package.json) - AGENTS.md, CLAUDE.md, the agent skills, .mcp.json, the .claude allowlist, abaplint.jsonc and abap2ui5lint.jsonc, the CI workflow and the scripts it runs. A file the project has is never overwritten (skipped, and named); package.json and .gitignore are merged and only gain entries, every value kept that differs from the template's a warning; the configs follow the project's .abapgit.xml STARTING_FOLDER, and nothing is written into that folder; a second run changes nothing; dry_run answers the same report and writes nothing. The answer: written, merged (with what was added), skipped (with why), warnings (kept values, an abaplint.json beside the new abaplint.jsonc, a missing source folder, the framework pin check:pin would fail on) and next. The template comes from the checkout or the read-only mirror, as for scaffold_app; every path it lists is checked before anything is written (a plain relative path, a shared file of the template, outside the source folder - else the whole call refuses), a symbolic link is never written through, and the file system root, the home directory, the server's installation, the template, the mirror cache and the workspace are refused as targets. A tool of its own rather than an option of scaffold_app, which hands a NEW project back and writes nothing. lib/agent-setup.mjs ports the template's create/agent-setup.mjs planner; test/agent-setup.test.mjs drives the tool over stdio (a fresh directory, an existing project skipped and merged, another STARTING_FOLDER, idempotence, the refusals, symbolic links, the pin check) and compares its plan byte for byte with the create package's when an app-template checkout is beside this repository.

  • Every abap2UI5 app is agent-operable: app_list, app_start, app_describe, app_act. interact_app drives an app with CSS selectors in a headless Chromium and answers with a picture; the new tools speak the abap2UI5 JSON protocol itself against the local backend - the app start, the event with its arguments, the model delta of what was typed, exactly as the UI5 frontend sends them - and answer with an agent snapshot v1: the fields an agent may fill (model path, label, kind, value, choice values, required, editable), the actions it may fire (event name, static arguments, row-dependent ones as $row:FIELD), the tables (columns, the first rows, selection mode, editable cells), the messages (toast, message box, MessageStrip, value states, the app's message table) and some static text, from the view XML of every open slot (main, nested, popup, popover) and the model. An act is validated against the snapshot before anything is sent - an unknown event, a field that is not on the screen or not editable, a choice outside its values is refused naming what is allowed; values without an event stay pending, as typing does in the browser; the popup close the browser performs alone is @CLOSE_POPUP. app_describe answers from the last response kept, no roundtrip. lib/snapshot.mjs (pure: the frontend's slot and model bookkeeping, and the snapshot), lib/viewxml.mjs (the view, binding, expression and event-wire parsers) and lib/appclient.mjs (the roundtrips, sessions and validation); the linter's UI5 metadata, when it resolves, classifies controls the snapshot has no entry for. The shape is shared with the VS Code extension and the ABAP agent addon: docs/agent-snapshot.md is its reference, including where it deviates from the contract it started from (no /XX/ two-way prefix in the current protocol, pending values, selectionField, @CLOSE_POPUP) and what it cannot see yet. test/snapshot.test.mjs and test/appclient.test.mjs run on eleven recorded sample sessions (test/fixtures/agent/, real request/response pairs of abap2UI5/samples apps on @abap2ui5/node-runtime 1.146.0) - the replay insists on the exact request the real run sent; test/agent-integration.test.mjs operates a form, a table with a row action and selection, and a popup on the published runtime, through the client and through the server over stdio (network-gated like test/npm-integration.test.mjs).

  • The app client embeds without wrappers. createAppClient takes transport (one roundtrip: the serialized request with its headers, timeout signal and the draft id it continues in, { status, headers?, body } out), location (the app start's ORIGIN/PATHNAME/SEARCH), an optional generation (absent: no restart detection) and backendHint (the words after "the backend did not answer (...)"). The defaults are the local backend's, so the MCP server's requests and refusals are unchanged (the recorded sessions replay as before); the VS Code extension, which vendors the client for a real SAP system, drops the request rewriting and message patching it needed. docs/agent-snapshot.md "Embedding the client" is the reference; test/appclient.test.mjs covers every option.

  • migrate_report: a classic ABAP report as an abap2UI5 app. The source of a report (and optionally its .prog.xml text pool and the class name) in; the class of the abap-cloud-gui addon out - INHERITING FROM z2ui5_cl_cgui_report, the selection screen as selection_screen( ), the event blocks as its methods, the list as write( ), the ALV as alv( ) - with the migration report (TODOs, unreleased tables and APIs with their successors, what was not carried over, every mapped construct) and the refusals (file:row:col and the reason; no class unless partial). The converter is the addon's report2cloud, imported from an abap-cloud-gui checkout (ABAP_CLOUD_GUI_HOME or ../abap-cloud-gui, npm ci done) - not bundled, not on npm, and reported with the clone command when it is missing (lib/siblings.mjs, setup_status lists it). deploy: true writes the class with the addon's src/01 and the popups it calls into the dev sandbox, builds the backend and answers app_start's snapshot of the selection screen, stage by stage like verify_app. lib/migrate.mjs; test/migrate.test.mjs converts a fixture report and a refused one (skipped without the checkout) and runs the deploy on @abap2ui5/node-runtime through Execute (behind the network gate).

  • Agent snapshot: selection dialogs and message lists. SelectDialog and TableSelectDialog are tables now (control names them, selectionMode from multiSelect, the dialog's title titles its layer), and their confirm is a row action: app_act({ event, row }) picks the row as a click does - its selectionField (selected="{ZZSELKZ}") set, the other rows' cleared in single select, all sent as the model delta - and fills the confirm's $parameters from the selected rows (selectedItem, selectedItems, selectedContexts). The row-valued parameters of table row events (listItem, rowIndex, rowContext, rowBindingContext, a row action item's row) are filled from row too, for the shapes views write (.getBindingContext().getProperty('X'), .getPath(), .get<Prop>(), .getCells()[n].get<Prop>(), the ? : guard); paths follow UI5's JSONModel, so selectedContexts[0]/sPath is null as in the browser. Anything else is refused naming args[i]. The items of a MessagePopover / MessageView are messages with source popover / messageview and the optional subtitle and description. All additive - snapshotVersion stays 1, and docs/agent-snapshot.md now says what may change within a version. New recorded sessions: samples- controls 623 (SelectDialog), samples 452 (MessageView, MessagePopover) and two abap-cloud-gui report2cloud reports (the F4 TableSelectDialog of the popups, the message popover); the agent integration test picks a row through app_act on @abap2ui5/node-runtime.

  • The agent client follows the protocol's frontend rules (abap2UI5/protocol frontend suite, adapter agent: 53 pass / 5 MUST failures / 3 warnings before, 61 pass / 0 / 0 now; the 20 skips are capabilities a client without a browser does not have). lib/appclient.mjs: a response declaring a PROTOCOL other than 2 is refused whole - its draft id, view and model - with both numbers named (an absent one is let through); the last sap-contextid response header of a session is sent with every later POST of that session (never empty, never undefined; a response without it keeps it), so a stateful ABAP app keeps its work process; a token layer's 403 + X-CSRF-Token: Required is answered by a HEAD token fetch and one re-send of the same body, the token then sent with every POST (a 403 without Required stays final); one roundtrip at a time per session - an app_act with an event while another is in flight waits for it and runs on the draft id it left, instead of posting the same draft id twice; a roundtrip clears only the edits it carried, so values set while it is in flight stay pending, survive its model push and travel next (a failed one rolls back only its own edits); the error body is shown verbatim - no tag stripping, no entity decoding, only shortened (40 lines, 4000 characters) and control characters shown as U+FFFD. lib/snapshot.mjs applyResponse: a response of another APP tears the popup and the popover down. Embedders: the transport option now also receives method ('POST', or 'HEAD' for the token fetch, without a body) and the sap-contextid / x-csrf-token headers to send; the client reads both from the answer's headers and does the handshakes itself, so a transport must not repeat them. New exports: PROTOCOL, headerOf, validContextId. docs/agent-snapshot.md points to the normative semantic profile (abap2UI5/protocol profiles/semantic.md).

  • Database tables in the npm backend's sandbox. A sandbox's transparent tables (TABL, with their data elements) are transpiled with the classes and created in the runtime's SQLite database at boot: the build takes their CREATE TABLE out of the transpiler's init.mjs and apps/init.mjs runs it after the package's boot, before the apps load (a structure is a TABL too and creates nothing; a sandbox without tables builds and boots as before; a transparent table the transpiler wrote no statement for fails the build with that reason). migrate_report { deploy: true } deploys abap-cloud-gui's tables and data elements with its classes - since its PR #8 the variant and layout stores (z2ui5_cgui_var, z2ui5_cgui_lay) are the default, and the deploy failed at app start with Void type: Z2UI5_CGUI_VAR; abap2ui5-unit deploys a project's *.tabl.xml / *.dtel.xml too, so a class under test can INSERT and SELECT its own tables (test/fixtures/table-app, run in test/npm-integration.test.mjs). A framework checkout (and the clone of A2UI5_MCP_BACKEND=clone, once build_backend made it) already created every deployed table through the framework's own setup; without a clone yet there is no sandbox, as before.

Since the 1.0.0 section was first written

Merged between 2026-10-03, when the section above was prepared, and the release:

  • interact_app no longer waits for networkidle. After the actions (each of which waits for its own roundtrip) it asked Playwright for no request of any kind for 500 ms, up to 10 s, before the quiet-backend tail - and a page with a long poll or a slow CDN download spent the whole 10 s there on every call. The tail is the backend-quiet wait alone.

  • The local UI5 lookup reads the @openui5 directory once. Every UI5 request of a boot (hundreds) re-read the corpus' node_modules/@openui5, stat'ed each package's src and then exists+stat'ed the file; libRoots is memoised on the directory (an install moves its mtime) and a file costs one stat per root (0.07 to 0.04 ms a request on ten roots, the directory read 0.04 to 0.008 ms).

  • A build or a backend stop keeps the Chromium. stopBackend closed the browser run_app/interact_app share, so every real build and every stop cost the next call a browser launch. It now closes the contexts open on the backend (their pages show a dead app) and leaves the browser to the shutdown (closeBrowser).

  • The warm lint worker exits when idle. With the framework parsed it holds about 300 MB, and it held them for the server's life. After A2UI5_MCP_LINT_IDLE_MS (default 10 minutes; 0 keeps it) without a request it exits, and the next lint starts a fresh worker (the cold parse once more); a lint that arrives while it exits starts one at once.

  • The GitHub mirror backs off exponentially. A failed download was tried again every 3 minutes, and a machine that is offline without A2UI5_MCP_OFFLINE stalled every mirrored tool call the 20 s fetch timeout that often, for as long as the server ran. The wait now doubles with each failure in a row per repository (3, 6, 12 min ... up to the mirror TTL) and starts over at a success; the hydrate answer carries failures and backoffMs.

  • npm test in well under a minute. The three tests that install the published runtime from the registry (npm-integration's loop and sandbox table, agent-integration's app tools, migrate's deploy) are opt-in under A2UI5_MCP_NETWORK_TESTS=1, which CI sets; the backend lifecycle tests wait for the port and the child's exit instead of sleeping 1.4 s after every stop, and turn the start's liveness wait and port poll down (backendTiming, a test hook, not an environment variable).

  • run_app / interact_app answer a failed UI5 bootstrap at once. With the CDN unreachable (and no local @openui5 packages) the boot waited its whole timeout - 60 s - although the bootstrap request had failed within milliseconds; the wait now ends the moment sap-ui-core.js fails (64 s to 2.8 s measured), with the same report. Under A2UI5_MCP_OFFLINE with nothing local to serve, the report is the answer before a backend or a browser is started.

  • UI5 from the CDN is downloaded once, not per call. Each run_app/interact_app boots in a browser context of its own, whose HTTP cache is empty, so every call fetched the bootstrap and a few hundred modules from sdk.openui5.org again. The CDN route now keeps its successful answers in memory across calls, bounded by A2UI5_MCP_UI5_CACHE_MB (default 64; 0 switches it off), the oldest evicted first; the local @openui5 packages and the offline 404 come before it, and the per-call context isolation stays.

  • A build nothing changed for is not made. build_backend (and the build stage of verify_app and migrate_report { deploy: true }) always stopped the backend and transpiled the sandbox - 7-8 s on the npm backend, 1-2 min on a checkout - and every app session on the backend died with the stop. The build record now carries a fingerprint of what the build was made from (the sandbox's files by name and bytes, the release, the transpiler version, the open-abap-core commit; on a checkout the transpile config and node/downport by name, size and mtime, recorded with the init.mjs the transpile wrote), and a build with the same fingerprint answers built: true, unchanged: true at once: nothing transpiled, the backend not stopped, its sessions intact. The stop (beforeBuild) moved into the build, called only when one really starts; syncDevCopies leaves a copy that already holds the sandbox's bytes untouched (its mtime is part of the fingerprint) and reports what it wrote under changed.

  • The lint runs warm. Every deploy spawned a fresh abaplint --format json over the sandbox and the whole framework, 2.6 s of which was the framework's parse, the same each time. One long-lived worker (lib/lint-worker.mjs, driven by lib/lint-host.mjs) now keeps an @abaplint/core Registry with the dependency folder parsed and lints a deploy by replacing the sandbox's changed files in it (900 ms cold, 9 ms warm against the real core); the npm backend installs @abaplint/core at @abaplint/cli's version beside it for exactly that. The answer is the CLI's JSON shape, byte for byte; the CLI runs whenever the worker cannot stand in for it (no core of the CLI's version - a framework checkout -, a config it does not mirror, a worker that died) and under A2UI5_MCP_LINT_WORKER=0. The timeout and the client's cancel kill the worker and read as before.

  • Less work per call across the cheap half. The repository resolvers' identify check parses catalogue.json once per version of the file (readCached, with a scope that keeps two parses of one file apart); examples computes each entry's haystack, missing terms and keyword score once before ranking instead of on every comparison; docs_search reads lowercased copies cached with the page slices instead of lowercasing the corpus per query; app_list reads the transpiled class modules once per build (memoised on the output folder's init.mjs); spawnWithTimeout accumulates a child's output in chunks and cuts the kept tail once at close instead of flattening it per chunk (quadratic at the lint's 64 MiB cap); validate_view's warm render gate keeps two pages, and A2UI5_MCP_RENDER_PAGES (1 to 8) sets it.

  • run_app and interact_app wait for the backend to be quiet, not for a fixed 600 ms. The tail after the boot and after the action script (quietAfter in lib/runtime.mjs, on the in-flight count trackRequests keeps) ends once every backend request has answered and none went out for 250 ms - at least 250 ms for the render to settle, at most the 600 ms it always was; the wait after an action (settle) is the same helper behind its 100 ms grace. Unit-tested on a fake clock.

  • The linter's 0.9 line is admitted. The peer ranges on @abap2ui5/linter and @abap2ui5/linter-render are >=0.8.0 <0.10.0 (were <0.9.0), so a project on linter 0.9.0 installs this server without an ERESOLVE, and npx -p @abap2ui5/mcp-server brings 0.9.0 along. The exports the server imports (., ./findings, ./config, ./rule-docs and the shapes behind them) are unchanged in 0.9.0 - the linter's own downstream job runs this server's smoke against it.

  • abap2ui5-unit refuses a path without classes before it installs anything. A typo in the path (or a run from the wrong directory) was found out only after the framework install and the backend build - a minute or more of a CI job, tens of minutes on a checkout's full build - and then ended with the same no *.clas.abap under ... and exit 2. The classes (and --class names) are read first now.

  • A backend that cannot bind its port says how to move it. With the framework checkout's own express server, a port already in use (3000 is every dev server's default) ended run_app, app_start and backend start with the bare listen EADDRINUSE line. The answer now adds which port is taken and that A2UI5_MCP_PORT moves it, as the npm backend's host already did.

  • validate_view and fix_view refuse a min_ui5 that is no version. Any string went through and was compared as if it were a floor: min_ui5: "abc" answered "newer than the abc floor", and a typo such as "1,120" judged the view against a floor nobody meant. The argument is checked the way the linter's CLI checks --ui5 (1.71, 1.120.0), and anything else is an error naming the value.

  • The npm backend builds on the transpiler 2.14 layout. abap2UI5's next release moves to @abaplint/transpiler-cli and @abaplint/runtime 2.14.2 (abap2UI5/abap2UI5#2864), which writes no flat output folder any more: the transpile's input goes into output/project/, each library into a folder of its own (output/open-abap-core/, output/express-icf-shim/), and a class imports another origin's module by a relative path into that folder (await import("../open-abap-core/cx_root.clas.mjs")). The build's rewriteImports only knew ./<file> and pointed it at @abap2ui5/node-runtime/output/<file>, which no longer exists - every dev app would have failed to build. The build now tells the two layouts apart (outputLayout): a flat release (1.146.0 and earlier) builds as before; a folder-layout one has its dev apps copied by the package's own setup/own-apps.mjs, run as a child, which points every import at @abap2ui5/node-runtime/output/<folder>/<file> - the package indexes its own folders, so mcp-server keeps no copy of that logic. apps/ stays flat (the runner's ./project/ test modules are rewritten beside it), a transpiler whose layout is not the package's fails the build, and the check for a stray import now also catches a ../<folder>/ one, on whatever wrote apps/. app_list finds class modules one folder down (the package's output/project/, a checkout's node/output/project/), and the prebuilt download refuses a backend-<version>.tar.gz whose layout is not the one the checkout's locked transpiler writes (a checkout past the switch whose version's release predates it) - auto builds the framework itself instead. The integration tests run on a local build of the package with A2UI5_MCP_TEST_RUNTIME_TGZ, and pin the property that matters: every import in apps/ resolves to a dev module or the package's own file, and a CATCH cx_root in the app's unit test catches a runtime exception. Proven on the package built from abap2UI5/abap2UI5#2864 (transpiler 2.14.2) and on the published 1.146.0 (2.13.93).

  • read_example and read_app count a file's lines right. Every abapGit file ends with a line break, and the line after it was counted as a line of its own: lines was one too many, and a from_line past the end answered an empty source under a pastEnd that promised the file's last line. The break after the last line now opens no line; the page that ends the file carries it, so the pages still join to the exact text.

  • docs_search refuses a blank query, and a pitfalls miss names its catalogue. A query of spaces searched for no term and answered a hint about its terms; it is now the same error as a missing query. A pitfalls query that matched nothing said "nothing in the all catalogue"; it says "either catalogue" for the default area.

  • A failed warm lint no longer leaves stale findings behind. The lint worker applies a deploy's changes to its registry one file at a time; when one of them threw, the changes before it stayed in the registry while the worker's record of the sandbox did not move, so a file changed back to what that record held was never updated again and its old findings came back on every later lint. A failed update now drops the registry, and the next lint parses afresh (the failed one runs the CLI, as before).

  • The answer guard shrinks in near-linear time. guardAnswer (and fitObject) halved the largest array or string of an over-budget JSON answer in a loop that re-serialised the whole value twice per cut, and a string already trimmed as far as it goes was picked again on every step until the 10,000-step guard: 2.5 s of CPU for a 474 KB answer of 480 rule explanations, and ~230 s for the property test in test/paging.test.mjs, which made it most of CI's test job. Every node is now sized once, the sizes are updated by the difference a cut makes, a heap names the largest, and a cut that changes nothing ends the loop: the same 474 KB answer takes 4 ms, the property test (still its 400 seeded cases, and now asserting that a shrunk JSON block parses) 3.5 s. The cuts are exactly the ones made before - same order, same notes, never over the budget - which a new test pins against the literal rule over seeded values at every budget a cut passes through.

  • The texts an agent reads say what the answers do. A drift sweep of the tool and schema descriptions, the prompts, the resources, the README, docs/agent-snapshot.md, SECURITY.md and server.json against the code: examples, docs_search, pitfalls, read_app and read_example declared their paging arguments (offset, from_line) without their descriptions saying they page, or naming pastEnd; validate_view and fix_view never named rulesCut, fixedCut or remainingCut, nor run_app errorsCut, screenshotCut (at 4096 px) or screenshotNotSaved, screenshot_view notAttached, interact_app notPerformed, run_unit_tests its per-object count; build_backend documented neither mode npm nor that a client's cancel stops it nor devAppsNotBuilt; migrate_report said nothing of the @KERNEL refusal, the dynamic-WHERE warning or the files it leaves out of a large answer; add_agent_setup nothing of the directories it refuses or the links it skips; scaffold_app nothing of its pages; the app tools nothing of how a large snapshot is fitted (and its note promised that max_rows pages a cut table - it does not; rowCount and app_act reach every row); the build prompt told the agent to read app_guide and pitfalls whole in one call, which pages; capabilities answers entries for a status alone too; the README named the screenshot file <class>.png and said booting needs no network without the corpus; SECURITY.md described the child environment by category and left out NODE_ENV, DEBUG, NODE_ICU_DATA, NODE_USE_ENV_PROXY and LD_LIBRARY_PATH; the boot timeouts' bounds were nowhere. Three tests keep it so: every paging argument a schema declares is named in its description, every ...Cut field the code answers is in AGENTS.md's vocabulary and in a description, and SECURITY.md names exactly the allowlisted variables.

  • A resource read fits the client too. resources/read handed the documents over whole with no guard, and two of them are past what a client shows of one read - abap2ui5://pitfalls/abap (105,000 characters) and abap2ui5://capabilities (62,000) - so a client that caps resources showed nothing. A document past the answer budget is now cut at a section heading and ends in a note naming the call that reads on from the first section left out (pitfalls { area: "abap", offset: 4 }, counted by the tool's own slicer); a JSON resource is shrunk as JSON; the MCP Apps screen, HTML a host renders, is never cut. The descriptions of the document resources say so.

  • run_unit_tests { class_name: "" } is refused instead of running everything. A blank name was read as no name, and on a framework checkout that runs the whole transpiled tree - minutes - for what was a name gone missing; it is refused now, naming what leaving the argument out does (optionalName in lib/args.mjs). And class_names: null is absent, as null is for every other argument, where it was refused as "not an array" (an empty list stays refused).

  • examples survives a catalogue field of an unexpected type. A catalogue.json entry with a numeric title (or an object summary, keywords that are a number) threw out of .trim() in catalogueEntries, and the repository's whole JSON was lost with it - the search fell back to SAMPLES.md, or failed outright over an injected catalogue. Every field is now coerced (a number or boolean as written) or dropped (an object, an array where text belongs); an entry whose class is an object or blank is skipped.

  • add_agent_setup refuses the home directory and the forbidden directories on Windows whatever their spelling. The checks compared paths case-sensitively, and on Windows neither the resolved path nor os.homedir() comes back in one canonical case - C:\USERS\ME was "not the home directory", a differently cased path into this server's installation not inside it. Under the win32 path semantics both sides are folded now (pathWithin), the same for the writes' own containment checks; POSIX stays case-sensitive. Tested with path.win32 over a fake Windows disk, so it is pinned on every machine.

  • system_status no longer reads a logon page as a working logon. Any 200 was "accepted the logon" - and an ICF node with a form or SSO logon answers Basic credentials it does not take with its HTML logon page, a 200. The answer is ok now only when the abap2UI5 start page answers (the z2ui5 component the framework's _http_get shell declares); a logon page is a problem that says the endpoint must accept Basic authentication, any other page one that asks whether the URL is the ICF service's path.

  • No tool answer can pass the client's size limit any more - a backstop at the one place answers are serialised. Every tool already fits its own answer (the paging vocabulary), but a composed or unbounded one could still add up past the cap, and a client handed more than its cap shows the agent NOTHING. guardAnswer (lib/budget.mjs) now wraps every tool result at the single dispatch point: an over-budget answer has its largest array fields and longest strings trimmed the documented way (a JSON block stays JSON; a __answerGuardCut note says it happened and how to page the rest), so no answer leaves over the budget whatever a per-tool fitter missed. Per-tool fitting stays the primary mechanism; this is the last line. A seeded property test drives 400 oversized results through it. Two spots that could reach the edge are also fixed at the source: validate_view (and so verify_app) caps explain: true's rule explanations (fitRules) - a view tripping a dozen distinct rules carried them all, and rules alone could pass the budget while only the findings beside it were ever shrunk (rulesCut now names what was left out, the most severe findings' rules kept first) - and migrate_report { deploy: true } fits its composed deploy/build/start stages together (fitObject). The guard wraps the WHOLE dispatch - server.mjs registers one tools/call handler, guardAnswer(await dispatchToolCall(...)), so the error answer of a throw (whose message can carry a build's output) passes it too - and it holds an answer's pictures to IMAGE_BUDGET as well (8 MB of base64, formerly screenshot_view's alone). test/answer-guard.test.mjs makes the guarantee fail-able: it checks the single registration in the source, and sweeps every tool name (sandbox and system mode, an unknown name, a bad argument) over stdio under a test-only loader hook that marks what guardAnswer returned - an answer that bypassed it carries no mark, and every content block is measured against its budget.

  • Security: a hostile checkout can no longer steer a read or a write outside it through a symbolic link. The path checks (safeRelPath, the sandbox name gate) stopped a .. or an absolute path in a STRING; they did not stop a symbolic link in the checkout itself, and a checkout is untrusted content (a sample cloned from GitHub, an app under deploy). read_example reading a "sample" that is a link to a file outside the repository, docs_search walking a page directory symlinked out of the docs tree (or into a cycle), and deploy_app / migrate_report's deploy / read_app writing or reading through a link planted in the dev sandbox all now refuse when the path - every symlink on it resolved - leaves the intended root (resolvedInside, now in lib/contain.mjs). A sweep of every file read and write found the rest, and they go through the same check now: the guide (app_guide and its resources), the interface (api_reference), both pitfall catalogues, the capability map, the porting brief (generation_rules), the sample catalogues examples searches, the docs tree's root itself (a docs -> ~ link walked the home directory), every template file scaffold_app serves (template.json may also no longer list ../ paths - both are answered as refused) and add_agent_setup copies into a project (and the check-pin.mjs it imports), the support classes migrate_report { deploy: true } copies, the sandbox the npm build hands the transpiler, the corpus copies the incremental build makes and the lint config it writes into a checkout. A DANGLING link counted as inside (its parent resolved inside) and a write through it creates its target wherever it points - it is outside now; and the sandbox and project writes open with O_NOFOLLOW (writeNoFollow), so a link that appears after the check fails the write instead of redirecting it. test/security.test.mjs pins each.

  • Security hardening: what transpiled ABAP can reach. open-abap's @KERNEL escape (WRITE '@KERNEL <js>'.) turns a literal into JavaScript once transpiled, and that code runs in the backend and unit-test children with process.env in reach. Running the app is trusted code (SECURITY.md); this is defence in depth, not a substitute for not deploying untrusted ABAP. Two changes:

    • The children no longer inherit this server's environment: they get an ALLOWLIST (appChildEnv in lib/runtime.mjs) - PATH, the home and temp directories, TZ and the locale, NODE_OPTIONS, the CA/TLS and proxy variables, NODE_ENV/DEBUG, the Windows system variables - plus PORT and the loopback HOST, never ALLOWED_HOSTS. A denylist of the secrets this server knows (the GitHub token, the system-mode credentials) would still hand over everything else a shell exports - cloud keys, registry tokens. Enumerated from what the children read (the npm host, the framework's express.mjs, the runtime and express - NODE_ENV and DEBUG are all of it - and Node itself; the Windows names from Node's documentation), and measured: deploy_app, build_backend, app_start and run_unit_tests ran end to end on the npm backend from a parent environment of decoys (GITHUB_TOKEN, AWS_SECRET_ACCESS_KEY, NPM_TOKEN, DATABASE_URL, the SAP password, ALLOWED_HOSTS=*) with an app and a test class that print Object.keys(process.env) through @KERNEL - both children saw only allowlisted names - and the framework checkout's backend booted and served an app the same way. The build, lint, git and npm children run no app code and keep the full environment. test/backend.test.mjs pins it, the spawned child included.
    • migrate_report refuses a report that contains the escape (comments aside), and any converter output that does, before anything is converted or written - with the place and the remedy. On an SAP system the statement writes a line of text, so no classic report has a reason to carry it, and report2cloud copies a refused statement into the partial draft as it is (a local class's WRITE), where a later deploy would run it; flagging it instead would leave the live escape in the files of the answer. open-abap has no other source-level escape (CALL '<kernel>' dispatches to a fixed list, GENERATE SUBROUTINE POOL and INSERT REPORT throw) - but @abaplint/runtime evaluates a dynamic LOOP AT ... WHERE (<condition>) with eval(), so a condition built from a selection-screen field is code injection on the backend. That is ordinary ABAP: converted, and flagged in a new warnings list. test/migrate.test.mjs pins both.
  • scope_of says when the OpenUI5 checkout is what is missing. Without the checkout samples-controls' scripts/scope-of.mjs reads the JSDoc from (OPENUI5_SRC, else fork-openui5 beside the corpus), every OpenUI5 entity came back "UNRESOLVED (no source .js found - check the entity name / fork checkout)", sap.m.Wizard included - an answer that reads like a misspelt name. It now says the checkout is not there, where it belongs, and that validate_view answers the 1.71 question for a view without it.

  • run_app and interact_app say when UI5 itself did not load. The UI5 runtime comes from the corpus' @openui5 packages or the CDN, and a failed load of it was in no report: with sdk.openui5.org out of reach and no local packages, the answer was "boot: page.waitForFunction: Timeout 60000ms exceeded" and a blank picture - nothing an agent could tell from a broken app - and an app that booted without its theme was ok with an unstyled screenshot. A UI5 bootstrap that did not load is now an error naming the URL, the reason and the remedy (and that app_start needs no browser); a booted app whose theme or a library did not load carries a ui5 note.

  • validate_view's hint is about the findings that are left. Any hint made it explain that "an event without a handler is intended when the roundtrip alone is the point" - beside an unused namespace or a spelled-out get_event_arg( 1 ), neither of them about events (bench task 13's reference, samples-controls 533) - and it never said that fix_view clears the findings marked fixable. It names the event case only when an event finding is there, and fix_view when a listed finding is fixable.

  • app_act says what to pass for a control id only the browser knows. A row button without an id of its own that opens a popover next to it (${$source>/id}, the shape of samples 052 and bench task 13) was refused with "cannot be read here - pass it in args[0]", and nothing said what value that could be: the id is a clone id UI5 generates. The refusal now says so and that any id will do for a backend that only anchors a popover to it - this client places no popover. In the vendored lib/appclient.mjs: the VS Code extension and abap2UI5/protocol pick it up with their next re-vendor.

  • examples finds a sample by the controls its view builds. The search read only the catalogue's own words - what a sample is filed under and the keywords somebody chose - so sap.m.Dialog found 7 of the 33 samples-controls ports that build one, sap.m.SelectDialog 2 of 8, sap.m.Table 23 of 62. samples and samples-controls commit catalogue-derived.json beside the catalogue: every control type each view builds. A query now matches those too, ranked below every match in the catalogue's words (whose order is the one without the file), and a hit that needed them names the controls it was found by under builds; foundByBuilds counts those hits - a generic word like button matches 135 more samples that merely build one. The GitHub mirror carries the file when the repository has it and is never refused for its absence; without it the search is the old one.

  • abap2UI5-bench no longer counts the app's own method as a client call (bench/, not part of the package). The expect check's calls matched any ->name(, and the canonical template names its helpers like the client methods they wrap - so me->popup_display( ) passed "calls: popup_display" for a solution that never called client->popup_display (a dialog sent through popover_display graded PASS). A call on me no longer counts; the new broken variant own-method-not-client pins it, and every reference still passes.

  • A first build_backend on a checkout builds the deployed apps too. With an abap2UI5 checkout and no prior build, mode auto downloaded the prebuilt backend - the framework alone - and answered built with next: run_app; the app deployed before it was in none of it, so app_start answered HTTP 500 "does not exist", app_list was empty and verify_app failed its boot stage on every first run. auto now goes on into the incremental build of the deployed apps in the same call (mode: prebuilt+incremental); an explicit prebuilt or transpile still builds the framework alone and names the apps it left out (devAppsNotBuilt, and a next that says how to add them).

  • docs_search counts every matching page. matches was the number of pages it returned - 10 for a query 37 pages answer - so a cut answer read as the whole site's, and nothing said where the rest was. matches counts every hit now, returned the page, and more names the new offset that continues it.

  • read_example pages a long sample by lines. It answered the file whole, and samples-controls' largest ports are 84,000 characters - past what a client accepts, so the agent read none of it. A long sample comes in pages of whole lines now (page, nextPage, from_line), measured as the answer is written, like read_app. A from_line past the end - in either tool - was clamped to the last line without a word, usually the empty line after the final newline, and read as an empty file; the last line still comes back, with pastEnd naming the line count.

  • An npm build that cannot write its files fails with a reason. A workspace whose open-abap-core/ or sandbox the build could not write (a file where the directory belongs, a read-only or full disk) threw out of the build: build_backend answered a bare ENOTDIR and build_log kept the previous build's record. The fetch and the transpile answer a sentence now, and the failed build is the one build_log shows.

  • An action list that is no list is refused with a sentence. A response whose S_ACTION.T_SYSTEM or T_CUSTOM was an object or a number made app_start / app_act answer "object is not iterable" - applyResponse threw a TypeError. The client now refuses such a response naming the list, and applyResponse folds it as an empty list. Both are in the vendored modules (lib/appclient.mjs, lib/snapshot.mjs): the VS Code extension and abap2UI5/protocol pick the change up with their next re-vendor.

  • A build_backend that joined the running build can be cancelled, and reports progress. A second call of the same mode joins the build in flight, and its own cancel was ignored: the request stayed open until the build ended (tens of minutes for a full one). It stops waiting now and answers as cancelled; the build goes on for the call that started it. And a joined call with a progressToken heard no progress notification until the build ended - it follows the build's lines now, until the build ends or the call is cancelled.

  • app_guide pages by chapter. The default call - the one its description tells an agent to make first - answered the whole guide, 48,000 characters of the 60,000 an answer may carry, and the guide grows upstream. Chapters that fit come back, more names the offset to continue at, and limit caps the chapters, like capabilities and api_reference; a chapter is never cut.

  • run_app and interact_app list a repeated error once. A page that throws from a timer, or a poll the backend answers 500, pushed the same line into errors for every occurrence - a 100 ms interval over a 60 s boot is 600 lines, 180,000 characters, and the client showed the agent nothing. A repeated message is listed once with its count ((600 times)), at most 20 distinct ones are listed and errorsCut counts the rest; ok still counts every error.

  • validate_view and fix_view fit a view with hundreds of findings. 600 broken buttons answered 538,000 and 584,000 characters, and the client showed the agent none of them. The findings are listed most severe first, in source order within a severity (they came in the order the gates produced them - a warning on line 140, a hint on 108, a warning on 26), and the ones that fit are the head of that list; counts stays whole and findingsCut / remainingCut say how many were left out; fix_view's corrected source and validate_view's rule explanations (explain: true, about 1,000 characters a rule) always go whole, and the findings get the room that is left. verify_app, which composes the tools' answers, fits its stages together too: a passed validate stage gives up findings first, then the unit stage counts its tests per object (a few hundred hints and tests were ~100,000 characters).

  • build_log, build_backend and read_app answers fit. build_log answered tail lines whatever their length (2,000 transpiler lines: 464,000 characters) - the last lines that fit come back now, a line past 4,000 characters shortened, and cut names the call that reads the rest. A failed build_backend put its 30-line tail into the answer unshortened (1.5 million characters for a build that prints a bundle per line). read_app paged by raw characters while the answer escapes every quote: a class of JSON templates went out at 73,000 characters.

  • interact_app refuses a step it would perform otherwise than asked. The tool's arguments are type-checked, its steps were not: { comit: false } and { commit: "false" } committed the fill anyway and { ms: true } waited 1 ms, each reported as performed. A step's unknown field, a commit that is no boolean and an ms that is no number are refused naming the step.

  • setup_status finds git, tar, npm and npx without which. It spawned which (where on Windows) once per program, with no timeout, and on an image without which reported every program missing. The PATH is scanned in-process now, with PATHEXT on Windows.

  • migrate_report { deploy: true } installs the template's abaplint. It runs build_backend, whose npm install reads app-template's @abaplint/cli pin, but the mirror is hydrated per tool the client called - on a machine without the template checkout the build fell back to ABAPLINT_CLI_FALLBACK. migrate_report hydrates the template like build_backend and verify_app do.

  • run_app and interact_app keep the 50 newest pictures, not every one: each call wrote a full-page PNG of its own name and nothing removed them. Only files of the server's own name pattern are removed, so a directory A2UI5_MCP_SCREENSHOT_DIR names keeps the user's files.

  • A failed boot setup no longer leaks a browser context. A throw of newPage, of the benign-rules import or of page.route in run_app / interact_app left the context open for the life of the server and a listener on the request's signal; both are released now.

  • A failed mirror download is not retried on every call. Without a checkout and with GitHub unreachable, every call of a mirrored tool waited out the 20 s fetch timeout again (examples, three mirrors, a minute). A failure is now the answer for three minutes - the stale copy when there is one, the reason in the missing-checkout message otherwise, with how long ago it was tried and when it is tried again.

  • A client's cancel reaches the app tools' roundtrip. app_start and app_act (sandbox and system mode) waited out the client's 120 s timeout after notifications/cancelled, holding the session's queue the whole time. The request's signal is combined with the timeout now (start/act take a signal option, so the vendored client keeps its defaults): the roundtrip is aborted, the act refused as cancelled and its edits taken back.

  • A port another process holds fails the backend start instead of handing the app tools to that process. Under express 5 the npm host's app.listen() called back with the bind error too: the host printed "Listening on", exited 0 a moment later, and startBackend's port wait was answered by whoever held the port - app_start and run_app then talked to a foreign server. The host listens on a plain http.Server now and exits 1 with "port N on 127.0.0.1 is in use by another process" (also over a release whose own serve() has the bug). And on both backends a child that exits right after "Listening on" fails the start: the port's owner answered the port wait before the exit was seen, and two starts in three still reported the backend running - the child now has to stay up half a second past its "Listening on" (the start ends early when it exits).

  • System mode never logs the password of a refused URL. With credentials in A2UI5_MCP_SYSTEM_URL (refused) or a URL that does not parse, the "ready in SYSTEM MODE" notification showed the raw URL, user and password included. It shows a masked URL now, masked up to the LAST @ - https://alice:pa@ss@host used to keep ss - and an @ before the query is refused as credentials (https://alice:12/ss@host parsed as a host alice and was shown as the endpoint).

  • run_unit_tests no longer throws on a long passing run of one class, and its answer fits. Past the budget it dropped tests and then read tests.length (a TypeError); and it measured compact JSON while the answer is indented, so 600 tests went out at 68,000 characters. Measured as written now (sizeOf); migrate_report's cuts the same way.

  • capabilities and api_reference are paged like examples: offset and limit, returned, a more that names the next offset, every entry whole. A broad query answered ~61,000 and ~75,000 characters. api_reference pages its compact list (no query) the same way, by entry, counting entries, and takes kind in the singular (method, constant, type), which it refused. Pages are measured at the depth their entries sit in the answer, examples and pitfalls too: a full examples page came out ~2% over the budget.

  • ALLOWED_HOSTS from the user's shell no longer reaches the backend. The framework's express.mjs reads ALLOWED_HOSTS=* as "answer any Host and any Origin", which switched its DNS-rebinding guard off for the dev backend.

  • The checkout backend, the corpus' e2e build, scope_of and the checkout's unit-test runner run on this server's own node (process.execPath), as the npm backend already did, not on whichever node the PATH finds first.

  • The npm host no longer mounts the release's compression twice (createApp({ compression: false }) behind its own compress); the responses are byte for byte the same.

  • Evicting an app session leaves a newer one with the same draft id reachable. Sessions started from one screen share its id (the playground's Pilot starts one per change of the reader's typing), and the eviction of the oldest made the newest an "unknown session".

  • Adaptive Cards: app text is never Markdown. The renderer is re-vendored from abap2UI5/protocol (with its shared renderers/common/ halves and the portable profile): a model value or a toast like [verify your account](https://...) was a live link in Teams and Copilot; a text with Markdown syntax is a TextRun now. Its htmlToText (a message box's details) is linear.

  • System mode: the app tools against a real SAP system. With A2UI5_MCP_SYSTEM_URL (the launch URL, {class} as a query parameter), A2UI5_MCP_SYSTEM_USER and A2UI5_MCP_SYSTEM_PASSWORD - or A2UI5_MCP_SYSTEM_PASSWORD_CMD, a command that prints it (a keychain lookup) - the server offers system_status, app_list (an ADT class-name search), app_start, app_describe and app_act against that system instead of the sandbox loop, logged on with Basic authentication: what the VS Code extension's system server does for its own window, for Claude Desktop, Claude Code, Cursor and any other stdio client. Same snapshot, arguments and refusals as the sandbox's app tools. A rejected logon is sent once and then refused without a request, so an agent's retries cannot lock the user. A2UI5_MCP_SYSTEM_INSECURE_TLS=1 accepts a certificate that cannot be verified. Without the variable nothing changes.

  • The app screen in the chat (MCP Apps). app_start, app_describe and app_act name the new UI resource ui://abap2ui5/app-screen (text/html;profile=mcp-app, SEP-1865 stable 2026-01-26) when the client advertises the io.modelcontextprotocol/ui extension - A2UI5_MCP_UI=on|off overrides. A host that renders MCP Apps shows the snapshot as a page the user can operate: fields, buttons, row actions, editable cells, messages. What the user does goes back as app_act calls through the host (validated like the agent's own) and is reported to the model with ui/update-model-context. The page is one self-contained HTML document - no external URL, no network - so it runs under the spec's default CSP.

  • Adaptive Cards. format: "adaptive-card" on the three app tools (or A2UI5_MCP_APP_FORMAT=adaptive-card) adds the screen as an Adaptive Card 1.5 - an embedded resource application/vnd.microsoft.card.adaptive - for Copilot/Teams-style hosts; off by default. Its submit payloads map to app_act arguments (docs/agent-snapshot.md). The renderer is abap2UI5/protocol's, vendored under lib/vendor/adaptive-cards with a source record and a sha256 drift test (scripts/vendor-adaptive-cards.mjs).

  • createAppClient gains screen(session): a copy of a session's folded state, for renderers other than the snapshot (additive; the vendored copies elsewhere keep working unchanged).

  • System mode: a wrong password reaches the system once under parallel calls too. Parallel tool calls all passed the breaker before the first 401 came back, so a wrong password was sent once per call (SAP locks a user after a few), and the password command ran once per call. Until the system has accepted the logon, requests go one at a time, and the command's answer is shared. system_status and app_list give up after 30 s instead of waiting for good, a redirect names its target (a SAML logon page), a malformed % in A2UI5_MCP_SYSTEM_URL no longer stops the server from starting, and a URL that does not parse no longer shows the user and password typed into it. A breaker refusal reads as itself, not as "the backend did not answer".

  • An act queued behind one in flight runs only where it still names what its caller saw. Two app_act { event: "a1" } at once fired the first screen's a1 and then whatever a1 was on the screen that answered it (NEXT, then DELETE_ALL - for real in system mode). The queued act is refused, sending nothing, when its action or fields name something else on the new screen.

  • A backend start during build_backend is refused. app_start, run_app or backend start booted the output from before the build, and the build then reported built while that process went on serving the old classes.

  • The app tools' answers fit the client. A 120 KB TextArea or a table of 200 wide rows went over the result cap by default. Long values are cut (and read-only in that answer), then rows dropped from the end, and a text beside the snapshot says what was cut. With format: "adaptive-card" the card counts together with the snapshot.

  • interact_app waits for each action's roundtrip. The frontend drops an event fired while a roundtrip runs, and its busy overlay shows only after a second: two quick clicks lost the second, and a click right after a fill was dropped - each reported as performed.

  • backend stop stops a start in flight. It answered "not running" while the start went on to listen, and a build that stopped the backend while one started reported built while the old process served on.

  • Pictures are bounded. run_app / interact_app cut a page taller than 4096 px (and say so); screenshot_view leaves out a picture over 8000 px a side or past its image budget, named. Each call writes its own PNG - two calls on one class reported each other's picture.

  • Answers that grew without bound are paged or cut: a lint's findings (the first 100, counted per rule - and the whole sandbox's JSON is read in full: one app with many findings made every later lint a parse failure), read_app (pages of whole lines, from_line), a full run_unit_tests (counted per object), migrate_report's files.

  • The build refuses a dev object named like a framework class - for every writer: deploy_app could not check before a release was installed, and migrate_report did not check at all.

  • An argument a tool does not declare is refused. app_act { value: {...}, event } fired the event without the edits. max_rows: true (or false, [], " ") is refused instead of read as 1 or 0, and an error repeats at most 80 characters of an argument.

  • App sessions are evicted least recently used first. The session started first went first, even while it was the one in use, and a session evicted while an act was in flight came back unlisted.

  • An event name without row fires the screen action of that name before a row action of the same name (a "delete selected" button after a table with a DELETE per row).

  • The Adaptive Card shows the snapshot's rows. It rendered every row of a table - 5,000 rows were a card of ~1 MB in the same result as the snapshot - and Save on the untouched card wrote back every row the snapshot did not show. A card over the answer budget is left out with the reason.

  • An untouched MCP Apps form sends nothing. A choice whose value matched no key showed - and sent - its first option, and a check box bound to null, '' or 'X' counted as changed. Message box choices with parentheses stay whole. Untouched now means "as rendered": a textarea with CRLF or a leading newline, a text with a newline and a multi-select in another order were written back on every press too.

  • The MCP Apps screen keeps what the user typed and the focus across a failed act and while one runs; it reads the current screen when its session moved on (Refresh failed for good once the agent had acted); a button that needs a value only a browser computes is disabled instead of failing every time; the reported height shrinks again; Refresh keeps the tool input's max_rows.

  • The snapshot survives &#99999999; in a view and unclosed < in model markup. The first threw the whole snapshot (after the act had reached the backend), the second took seconds for 80k characters.

  • A deploy lint reports the deployed class's findings only. They were picked from the repository-wide abaplint run by a substring of the path, so z_app was handed the findings of zz_app (any class whose name ends in its own) and a clean class failed deploy_app - and verify_app stopped at deploy. Matched on the file name now (issuesOfClass).

  • A backend that cannot be spawned fails its start at once. With no node on the PATH the server was started with (a desktop client's minimal one), the backend child emitted only error: an uncaught exception, and run_app / app_start / backend start waited 30 s to report "did not start" with an empty output. The start now fails with the spawn error right away.

  • read_example re-fetches a mirrored sample once it is a day old. A file fetched on demand into the GitHub mirror was served from the cache whenever the mirror's marker was fresh - and the marker is refreshed by every knowledge tool once a day, the on-demand files never - so a sample read once stayed at that first copy for good. The file's own age counts now (A2UI5_MCP_REMOTE_TTL_MS).

  • server.json lists POPUPS_HOME. migrate_report { deploy: true } reads it (through an injectable env parameter), and scripts/check-server-json.mjs only looked for process.env.X, so the variable was missing from the registry listing without the gate noticing. The scan reads env.X too.

  • setup.sh reuses a corpus checked out as abap2UI5-api. Its list of pre-rename directory names left that one out (lib/repo-dirs.json has it), so such a checkout was cloned a second time as samples-controls. A test now holds the script's lists against the JSON.

  • remove_app removes every file of the class. It removed the source, the sidecar and the test include, and left the local-class includes (locals_imp, locals_def, macros) that migrate_report { deploy: true } writes - the class disappeared from the deployed-apps list while its includes stayed in the next build's input.

  • The incremental build no longer clones open-abap-core synchronously. The git clone (and the corpus' patch script) ran through execFileSync: the stdio server answered nothing for the length of the clone, a cancel or the shutdown could not stop it, and a failed clone threw out of build_backend without a build_log record. Both steps are spawned like every other child now - cancellable, killed with their tree, reported and logged; a failed step takes the half clone with it.

  • Progress notifications keep their one-per-second throttle. A child's output was handed to the line callback through forEach, which passes the line's index as a second argument - and the server's progress reporter reads its second parameter as force. Every line after the first of an output chunk bypassed the throttle, so a chatty lint or unit-test run sent one notifications/progress per line.

  • A child's output reaches the build log and the progress messages in whole lines. Lines were cut at the pipe's chunk boundaries (one line in two halves, a UTF-8 character as two replacement characters); each stream is decoded as UTF-8 and buffered to its newline now.

  • The backend's stderr is no longer kept for its whole life. The start collects the child's output to report a failed start, and went on appending every line the backend wrote to stderr after it was listening - memory the server never released during a session.

  • validate_view / fix_view check allow. A bare string instead of the array the schema names was passed to the linter as it was: beside a project config its characters became the allow list (the allowance silently ignored), without one the gate failed on allow.map is not a function. Refused by name now, like every other list argument.

  • examples { area: "samples" } finds the samples again. The area was read off a src/01 path prefix, and abap2UI5/samples flattened src/01 and src/00 into one src/ package on 2026-09-22 - so every sample of a current checkout was filed as experimental-or-test, and the filter the description recommends answered nothing. src/00 decides now; an older checkout reads as before.

  • Boolean and object arguments are checked against the schema. The handlers read booleans as === true / === false, so a client that sent "true" got the default without a word: add_agent_setup { dry_run: "true" } wrote into the project it was asked only to plan for, migrate_report { deploy: "true" } deployed nothing, verify_app { boot: "false" } booted. screenshot_view's model as a string was spread into the derived model one character per key. Refused by name now, like a string argument that is not a string.

  • run_app / interact_app recover from a Chromium that went away. The browser was kept for the server's whole life: one that crashed or was killed was handed out again, and every later call failed on "browser has been closed" until the server was restarted; a launch that failed (no Chromium installed yet) stayed the answer after the install. A disconnected browser and a failed launch now clear the slot, and the next call launches afresh.

  • The npm backend's install never rejects, as documented. A workspace the install could not write into (read-only, a file where the directory belongs, a full disk) threw out of ensureRuntime: build_backend ended without a build_log record, deploy_app's lint answered a bare ENOTDIR, and every caller queued behind the failed install was rejected with it. It is the install's reason now, naming the directory.

  • docs_search survives a dangling symbolic link in the docs tree. The walk statted every entry, and one link to a file that is not there (a half-finished pull, a moved page) threw ENOENT out of it - every search failed. Such an entry is skipped now.

  • A mirror refresh takes out what the repository no longer lists. The daily refresh of a GitHub mirror wrote the new file list over the old one and removed nothing, so a docs page renamed or deleted upstream stayed in the mirror and docs_search - which walks the mirror's tree - kept answering with it (and a URL that 404s). Files the previous refresh listed and this one does not are removed now; a sample read on demand is not one of them.

  • Two run_unit_tests calls at once run their own classes. The filtered copy of the runner was named after the selection alone (index-mcp-selection.mjs for any set of classes), so two runs over different sets wrote one file: the first answered with the second's tests, and the first to finish deleted the file under the other. Each run writes a copy of its own now (index-mcp-<class|selection>-<pid>-<n>.mjs).

  • npm test no longer writes into the user's build log. Four test files ran builds without A2UI5_MCP_SCREENSHOT_DIR, so the fake build's last-build.json landed in the real <tmp>/abap2ui5-mcp-screenshots - a freshly started server's build_log then answered with a test's run as the user's last build - and the cancel and progress tests fetched the app-template mirror into the user's cache. Each test keeps both in its own temp dir now; the runtime default is unchanged.

  • app_act fires the action it was asked for when its values renumber the actions. Action ids follow the document order, and the act re-read its action by id after applying values: a value that showed a control in front of the action (a checkbox making a "Delete" button visible) made { values: { "/SHOW": true }, event: "SAVE" } send DELETE - an event nobody asked for and no check had seen. The act now fires the action it validated, found again on the new screen, and refuses when that is no longer one action (as the abap2UI5 agent addon does).

  • An interact_app fill of a missing element fails within action_timeout_ms. Before filling, the action looked whether the located element is itself editable - with Playwright's default timeout, so a fill of an id that is not on the page waited 30 s for it and then the fill's own timeout on top (40 s at the default 10 s, whatever the argument said). The look runs within the action's timeout now, and an element that never appears fails the action there.

  • verify_app's progress only goes up. Each stage made its own progress reporter for the one request, counting from zero, so the notifications of one progressToken went 1, 3 (the deploy lint), then 1 again (the build) - the MCP spec says the value MUST increase with each notification. The counter (and the one-per-second throttle) belongs to the request now.

  • abap2ui5-unit no longer reports an unreached class as having no test method. The generated runner stops at the first failing test, so a test class after it in the runner's order printed nothing - and the summary said "a test include, but the runner found no test method - is the local class FOR TESTING?", sending the reader after a problem that is not there. Such a class is notRun now (in --json too), naming the failure the run stopped at.

  • abap2UI5-bench keeps the agent's transcript in whole characters (bench/, not part of the package). Its child-process helper turned each output chunk into a string on its own, so a character the pipe split between two chunks - an em dash in the agent's text - became replacement characters in transcript.jsonl, the result text and the grader's output. The streams are decoded as UTF-8 across chunks now, and verify.mjs checks it.

  • The server stops when its client stops reading. A client whose end of the server's stdout was gone (EPIPE) while stdin stayed open left the server in a loop: the failed write was an uncaught exception, the crash report went out as a logging notification into the same broken pipe and failed again - about 90% of a core and a growing pile of drain listeners, for as long as stdin stayed open. A broken stdout now shuts the server down the way a closed stdin does.

  • app_act's args schema declares its items. It was an array schema without items - valid JSON Schema, and refused by clients that check a tool's schema before offering it (VS Code: "array type must have items"; OpenAI-backed hosts: "array schema missing items"). items: {} - any value, as before - and a test holds every array of every tool schema to it.

  • Resource and prompt errors carry the JSON-RPC codes the MCP spec names. An unknown resource URI (or guide chapter) answered -32603, an internal error of this server, where the spec says -32002 (resource not found); an unknown prompt and a missing required prompt argument answered -32603 too, where it says -32602 (invalid params) - as does a chapter URI with broken percent-encoding, which answered a bare "URI malformed". The messages are unchanged; a missing checkout stays -32603.

  • A late drop of a dead warm renderer no longer closes its successor. When two validate_view / screenshot_view calls shared a warm Chromium and both saw it die, the first dropped it and the next call opened a fresh one - and the second drop then closed that fresh renderer under the call using it, which saw a dead browser in turn and fell back to the cold path. A drop now names the renderer it saw die and leaves a newer one in place.

  • A build_backend refused for a build in progress keeps the backend running. The tool stopped the running backend before the build checked for an in-flight build of another mode, so a call answered "build in progress" had built nothing and still taken the app down. The backend is now stopped once the build really starts (buildBackend's beforeBuild).

  • A migrate_report { deploy: true } the sandbox refuses writes nothing. A class_name report2cloud accepts but the sandbox does not (a namespaced /abc/cl_x, or one outside z/y) was refused only after the report runtime's support classes had been copied into the sandbox, so the refused deploy still changed the next build's input. Every name is checked before the first file is written now.

  • screenshot_view refuses a zero-size viewport. sizes checked the digits (\d{2,5}) but not their value, so 00x10 passed as a viewport 0 pixels wide - which Chromium's device metrics override reads as "no override", a picture of its default window. An edge under 10 pixels is refused by name now, like one over 4096.

  • validate_view renders cold when the warm renderer throws. A warm Chromium that failed mid-call with a throw (a wedged page its dead browser could not reload) was dropped, but the throw went on to the "render gate could not start" fallback: the answer was the property findings alone - ok: true for a view whose render errors were never looked for - with a note to install @abap2ui5/linter-render. The call is retried cold now, as it already was for a dead browser that did not throw, and as screenshot_view does (warmThenCold in lib/validate.mjs).

  • A shutdown during a backend start leaves no backend behind. The backend child was only tracked once it printed "Listening on", so a client that went away while backend start / run_app was booting it had the server exit with the child untracked - and the child then listened as an orphan holding the port for the next session. The shutdown now also kills the child of a start in flight (stopBackend({ starting: true })); backend stop and restart are unchanged.

  • A binding's model key names its model in the agent snapshot. The object syntax {path: '/A', model: 'other'} - UI5 binds it to the model other - was read as /A of the default model: the field was listed editable with the default model's value, a table bound that way got editable cells, and an app_act value was written to /A (or /T/0/N) of the app's own model, which the screen does not edit. It is read like {other>/A} now (noted as bound to a named model), as the abap2UI5 agent addon's snapshot already does; docs/agent-snapshot.md says so. The copies the VS Code extension and abap2UI5/protocol vendor pick it up with their next re-vendor.

  • An edit the old view never sent dies with it when the view is displayed anew. app_act re-applied every unsent edit after each response, also into a page or dialog the response displayed again - where the frontend builds a new model with nothing pending. A value typed into the page behind a popover (or while a roundtrip was in flight) overwrote what the new page was sent, stayed pending and went out with its next event, into another app's model too. Only a model push keeps unsent edits now (rebuiltModels in lib/snapshot.mjs); docs/agent-snapshot.md says so.

  • A model push no longer makes up table rows for an unsent edit. The re-apply of a pending edit created every missing step of its path, so a cell edit of row 5 in a table the push shrank to two rows made it six rows long, with empty rows the snapshot listed and the next delta sent as the edited row. It is re-applied as JSONModel#setProperty does now - only where its parent exists; the path stays pending, as in the frontend.

  • A backend error body padded with whitespace no longer stalls the server. The refusal text trimmed its end with /\s+$/, which is retried from every position of a whitespace run that is not at the end: quadratic, 15 s of a blocked event loop for a body with 100k blanks and 100k newlines inside it. trimEnd() removes the same characters in linear time.

  • The build-an-abap2ui5-app prompt names the keys app_act takes. It told the agent to "fill fields by label", and app_act resolves a values key by field id, model path or attribute name only - every value keyed by its label was refused. The brief says so now.

  • A view cannot make app_act write into the server's prototypes. setAt walked a model path through whatever each step answered, and a plain object answers __proto__ with Object.prototype: a field bound to {/__proto__/shell} - view XML is the backend's, a real system's when the client is embedded - turned a value typed into it into Object.prototype.shell for every object of the process (the card renderer's way back wrote card payload keys the same way). A path through __proto__, constructor or prototype is refused by app_act now and never written by setAt; docs/agent-snapshot.md says so. The copies the VS Code extension and abap2UI5/protocol vendor pick it up with their next re-vendor.

  • An element named __proto__ no longer breaks the agent snapshot. The control tables were looked up by plain index, so a table cell or list item element named __proto__ (or __defineGetter__) found Object.prototype and was read as a field spec without properties - a TypeError, and app_start / app_describe / app_act failed for that screen. Only the tables' own keys are read now.

  • app_start names the session it started when its values are refused. The app was running and kept in the client, but the refusal carried no session id, so it was out of reach and the agent's next app_start opened a second one. The refusal ends "(the app is running: session - app_describe shows it)" now, as the ABAP agent addon's does.

  • A number field takes a number, not whatever Number() makes of a value. app_act read a value for a field whose model holds a number with Number(), so true went out as 1, "0x10" as 16, "1e3" as 1000, and "Infinity" as null - the field's initial value, without a word. A number or a decimal string is taken now, as the agent addon's describe_arg reads it; anything else is refused naming the field.

  • app_act's row is a row index, not whatever Number() makes of it. row: "", false or [] acted on row 0 and row: true on row 1

    • a pick or a row action on a row nobody named. row is a non-negative integer now (a number, or a string of digits); anything else is refused.
  • Catalogue rows, doc headings and view wires parse in linear time. Their patterns retried every split of a run of blanks between two \s* (or a lazy group and \s*$): a SAMPLES.md row with 1000 blanks and no sample link - a padded header row - blocked the server for 85 s in examples (read from SAMPLES.md where no catalogue.json is), a heading line with 10k blanks took 0.4 s in docs_search, and an .eB(...) attribute with a long run of blanks after its parenthesis 14 s per 100k in the app tools. The rewrites match the same lines and capture the same text; a test holds them to the old patterns.

  • build_backend prebuilt, transpile and full run npm on Windows. The framework checkout's npm ci (before serving a downloaded backend) and its own build's npm ci / npm run downport / npm run auto_transpile spawned npm without a shell. On Windows npm is npm.cmd, which spawn does not find that way (ENOENT), so those modes failed at their first npm step there. They go through the npm backend's spawnNpm now, as its own installs always did; a test holds every npm spawn of the server to it.

  • A long app session no longer grows the server's memory with every act. Each roundtrip answers a new draft id, and the client kept every one a session ever had, to refuse it as "an earlier state" - about 1.5 MB per 10,000 acts that a long-running server never gave back. The last 100 earlier ids are named as earlier states now; an older one is refused as an unknown session, naming the open ones.

  • Catalogue rows and markdown links parse in linear time. Two patterns were still quadratic in text from a mirrored repository: the SAMPLES.md row tried the sample link after every end a text block could have, and scanned to the next ) each time (0.4 s for a 50k row of "|A; the markdown link pattern docs_search flattens snippets with (and examples reads a row's docs links with) scanned to the next ] from every [ (3 s for 50k of them). Both are hand-written scanners now that match what the patterns matched - fuzzed against them, and identical on the real catalogues and the docs site.

  • A framework checkout's backend listens on 127.0.0.1 only. Its node/srv/express.mjs binds every interface unless HOST is set, and the server started it with PORT alone: beside a checkout, every deployed app (and the framework's own) could be run by anyone who reached port 3000 on the network, while the npm backend has always bound loopback. The backend is started with HOST=127.0.0.1 now, also over a HOST the user's environment exports.

  • The README's timeouts and popups lookup match the code. The timeout note left out A2UI5_MCP_UNIT_TIMEOUT_MS (the unit-test runner's 10 min), and migrate_report's popups were said to be looked for in build/popups before ../popups - the server takes .deps/popups, ../popups, then build/popups. RELEASING.md's tarball list now names the abap2ui5-unit script and action.yml it ships. A test holds the three to the code.

  • Security: the GitHub mirror's default base must be the user's own. <tmp>/abap2ui5-mcp-remote has a fixed name under the temp dir - /tmp on Linux, writable by every local user - and the server took whatever it found there. Another user who created the directory first, or a symbolic link of that name, decided what the knowledge tools served (the guide the agent follows, a template.json whose files add_agent_setup writes into the user's project), and the mirror's writes and the removals a refresh makes followed the link into the user's own files. The default base is now used only while it is a real directory owned by the user and writable by nobody else: it is created 0700, one of the user's own that others can read is narrowed to 0700, and anything else is refused with the reason and A2UI5_MCP_REMOTE_DIR (which stays unchecked) as the way out.

  • Security: the default screenshot dir must be the user's own as well. <tmp>/abap2ui5-mcp-screenshots holds the PNGs of run_app and interact_app and the persisted build log, under the same fixed name in the shared temp dir: another local user who created it first read every screenshot of the user's apps and handed a restarted server's build_log a last-build.json of their own, and a link of that name took the writes wherever it led. The default is used only under the same rule as the mirror's base now; otherwise the screenshot is still returned, not saved (screenshotNotSaved says why), and no build log is written or read there.

  • app_act: an act whose values disable or hide its own action is refused. The action was checked for enabled before the values were applied, and fired after them without asking again: OPEN set to false together with DEL pressed a Delete button with enabled="{/OPEN}" the browser had just disabled. And values that hid the action left its id with nothing behind it - the old snapshot's entry was fired anyway, a control the screen no longer shows. Both are refused now before anything is sent, with the values taken back; an action that only moved to another id is still fired as itself. The agent addon refuses both the same way.

  • app_act: the whole event parameter object is asked for in args. ${$parameters>/} and ${$parameters>} name the whole parameter object of a row event, which holds the row as an item or a binding context - and the client's internal markers for those ({"selectedItem":{"$item":0}, ...}, {"rowIndex":0,"rowContext":{"$ctx":0}}) went out as the event argument, where the browser sends the controls marshalled. A value that holds an item or a context anywhere is refused naming args[i] now, as the agent addon does.

  • Security: the npm backend's host serves loopback requests only. It listened on 127.0.0.1 and answered any Host and any Origin: a web page the user opened could post events to the dev apps, and through DNS rebinding (a name of its own resolving to 127.0.0.1) read the answers as a same-origin page. lib/npm-host.mjs now answers only requests addressed to 127.0.0.1, localhost or [::1] and, when they carry an Origin, sent from a page on one of those - run_app's Chromium and the app tools are; anything else gets a 403.

  • The server reaches its own backend at 127.0.0.1, never at localhost. The backend binds 127.0.0.1, but run_app's and interact_app's Chromium opened http://localhost:<port>, the start waited for the port at Node's default host (localhost) and the page's backend errors were picked out by the name localhost. Where that name resolves to ::1 first, those connections reached [::1]:<port> - which another local user can listen on, serving the page and answering the start - or nothing at all. All of them use BACKEND_HOST now (appStartUrl, waitPort, isBackendUrl, backendBaseUrl); the npm host's Host/Origin guard accepts what the Chromium sends from that address.

  • Downloads from GitHub are capped in size, not only in time. The mirror's files, the docs tree listing and the framework's release list were read whole, and the prebuilt backend archive written whole, within their timeouts - a proxy or mirror answering with a huge or endless body filled the memory or the temp disk first. Text answers are read up to 8 MB (TEXT_MAX_BYTES, readCappedText; the largest real one is 0.34 MB), the archive up to 200 MB (PREBUILT_MAX_BYTES; 2.7 MB for 1.146.0): one that declares more is refused unread, one that streams more is cut at the cap, and the tool says so by URL. A prebuilt download cut before its file was open no longer raises an uncaught ENOENT from the cleanup.

  • Dependencies: @modelcontextprotocol/sdk ^1.32.1 and playwright ^1.64.0. abap2UI5-bench keeps its gate versions (@abap2ui5/linter 0.8.3, @abaplint/cli 2.120.60): they follow the app-template commit bench.config.json pins, and moving them starts a new series.

  1. v0.3.0

    Other 32

    Notes
    • npm 12. npm view --json prints an array there, and the registry lookup read it as an object without a version: every lint, build and unit run without a pinned release failed with "the registry could not be asked". The lookup takes both shapes.

    • The expensive half runs on the npm package @abap2ui5/node-runtime - no framework clone. Without a framework checkout, build_backend shallow-cloned abap2UI5 and ran npm ci of its devDependencies there (205 packages, 186 MB of node_modules for 1.145.0: Playwright, @ui5/cli, eslint, ...) just to get a transpiler and abaplint, then downloaded the release's backend. The default backend without a checkout is now the package: installed once per release into ~/.abap2ui5-mcp/runtime/<version> with the transpiler it records, express within its peer range and app-template's abaplint for the lint - exact versions, a lockfile, npm --ignore-scripts; open-abap-core fetched with git at the commit the release was built with; and every build transpiles ONLY the deployed apps against the package (7-8 s), whose imports of framework modules are pointed at the package's own so the runtime never holds a second copy. deploy_app (into ~/.abap2ui5-mcp/sandbox, linted with app-template's config against the package's sources), build_backend, run_app, interact_app, run_unit_tests (the apps' own tests, never the framework's suite) and verify_app need no checkout at all. The release is the registry's latest, asked through npm and cached a day, unless A2UI5_MCP_RUNTIME_VERSION pins one; a release that exports accelerate() or compress gets them used. A framework checkout (A2UI5_HOME, a sibling, a workspace clone made earlier) keeps every behaviour it had; A2UI5_MCP_BACKEND=npm runs on the package beside one, =clone restores the clone as the default, build_backend mode npm builds on it explicitly and modes prebuilt/transpile clone the framework when there is no checkout. setup_status reports which backend is in use and why, and for the package the release, workspace, tool versions, open-abap-core commit and what the next build would do. Measured here, cold npm cache: the install 10 s, the first build 8 s, a warm build 7 s, the unit tests 1 s.

    • abap2ui5-unit and the GitHub Action run on the package too. Against app-template's starter app: 16.3 s cold and 8.2 s warm, where the clone took 26.9 s plus 3.2 s for the action's npm ci and 23.2 s warm (the whole framework was transpiled twice per run); the workspace is 67 MB instead of 231 MB. The action no longer runs npm ci - the script imports none of this package's dependencies - takes a backend input and caches the workspace per backend and pin. The release is --framework, else A2UI5_MCP_RUNTIME_VERSION, else the project's abaplint.jsonc pin, else the latest; --backend clone keeps the old path, which a pin without a package (before 1.145.0) and a branch take by themselves. A workspace clone an earlier version left behind is no longer picked up unless the clone is asked for.

    • abap2ui5-unit tests every class, and a class it cannot test fails the run. It deployed each class through deploy_app's gate, which asks for z2ui5_if_app: a helper or model class was reported "not deployed", its tests never ran - and the run exited 0, a green CI over a failing test. Interfaces and local-class includes were never deployed at all, so a class using one did not transpile. Every class and interface under the paths is now deployed with all of its files as the repository carries them, --class narrows the tests that run (not what is deployed), and an object that cannot be deployed (a namespaced name) exits 2. On the package the run builds in a sandbox of its own: it used to share the MCP server's, so an unfinished app an agent had deployed failed npm run test:unit, and the run deleted the session's copy of every class it had tested.

    • A failing setup or class_setup is reported as its own class's failure. The generated runner prints a test's line only after the test class's constructor and setup ran (and class_setup before the class's first line), and the failure was pinned on the test printed last: another class's PASSING test showed as failed in run_unit_tests and in the abap2ui5-unit summary, and the class whose fixture threw read "a test include, but the runner found no test method". The stack now names the fixture (failed.fixture, with the method that threw), and the runner keeps 100 stack frames instead of 10, so a failure deep in the code under test keeps the test class's frame. run_unit_tests on a class whose class_setup threw no longer answers "no test class of X in the built backend - deploy_app with testclasses": that hint is for a run that passed.

    • build_backend refuses a checkout build under A2UI5_MCP_BACKEND=npm. Mode npm was already refused beside a checkout in use, but the mirror case went through: with the package chosen as the backend, modes prebuilt, transpile and full built a checkout (or cloned one first) and answered built: true, while run_app and run_unit_tests kept running on the package and said "backend not built". They are refused now, before a running backend is stopped, and setup_status's hint no longer suggests them there.

    • A relative A2UI5_MCP_WORKSPACE works. It was used as given, and the npm backend hands workspace paths to children that run in another directory: with A2UI5_MCP_WORKSPACE=.abap2ui5-mcp the lint and the transpiler failed with "Cannot find module" on a doubled path. It is resolved against the directory the server runs in now.

    • deploy_app refuses a name the framework or the corpus already defines. The customer namespace a dev app may use holds the framework's own z2ui5_* classes and interfaces too, and zcl_sicf, the ICF handler every host boots. A dev app of such a name was written and built beside the original: a second copy that failed the transpile, or - on a checkout, whose incremental build copies the sandbox over the output

      • replaced the framework's class in what the host served. The name is checked against the release's downport/ (npm backend), src/ and node/srv/ of a checkout, and the corpus' own samples, and the refusal says where the original is.
    • On Windows a timeout or a cancel ends the whole child tree, and no console window opens. Windows has no process groups, so killing a child ended that child alone - and npm runs through cmd.exe there, so a timed-out or cancelled install ended the wrapper while npm and the git it started kept running, holding the output open: the call returned only when they had finished on their own. The tree is ended with taskkill /T now. And a server a desktop client starts has no console, so every child it spawned (npm, git, the transpiler, the backend) opened a console window of its own; they are started hidden.

    • setup_status names the abaplint install the next build makes. After abap2ui5-unit installed a release (without the lint's @abaplint/cli), or after app-template moved its pin, the next build_backend ran an npm install that nextBuild did not mention.

    • A build killed half-way no longer leaves its output behind for good. The npm backend's build, open-abap-core fetch and abap2ui5-unit run clean up in a finally that a killed process never reaches - and closing the MCP session mid-build is such a kill: every one left a staging directory of up to 22 MB in ~/.abap2ui5-mcp/runtime/<version>. Their names now carry the process id, and the next build removes those whose process is gone.

    • A cached "latest" the registry no longer has is asked again. The registry's latest release is cached a day; when the release it named could not be installed any more (unpublished, or npm pointed at another registry since), every lint and build failed with npm's "notarget" until the cache expired. The registry is now asked again, once, and the release it names now is installed. A pinned release is never swapped for another.

    • The registry lookup and the install read the same npm config. npm view ran in the directory the server was started in, the install in the workspace: a project .npmrc there decided which registry was asked for the latest release, never which one it was installed from. Both run in the workspace now and follow the user's npm config and npm_config_*.

    • The GitHub Action no longer runs its paths input as shell code. The test step read set -- ${{ inputs.paths }}, and the runner pastes an expression into the script before bash parses it: a paths of src; exit 0 # passed the step without running a test, and any $, ; or backtick in a path was code. Every input now reaches the scripts through env:, and the pin and backend the cache key is made of are reduced to a name before they go to $GITHUB_OUTPUT (a newline in the framework input was a second output line).

    • Answers fit the client. Claude Code refuses a tool result over 25,000 tokens, and three defaults went far over: scaffold_app (~280 KB, the whole template), pitfalls without a query (~120 KB) and examples with a large limit (~135 KB at 200). Each is now paged at about 60,000 characters (lib/budget.mjs) and names the arguments that fetch the rest: scaffold_app returns every file that fits, smallest first (the class, its sidecar and the configs always), lists the others under remaining and takes them back through the new files argument; pitfalls pages by whole section and examples by entry, both with a new offset. examples' matches is now the total, with returned for the page.

    • setup_status sees a Playwright-managed Chromium. It only probed three hard-coded paths (one of them the /opt/pw-browsers link of a sandbox image) and reported a machine with npx playwright install chromium done as having no browser. resolveChromium asks Playwright for its own executable after A2UI5_MCP_CHROMIUM and CHROMIUM_BIN (the linter's variable, now honoured by run_app too), keeps the system paths as the fallback with the sandbox link last, and setup_status reports the source.

    • A failed verify_app is a failed call. It answered isError: false with ok: false inside when a stage stopped it; a client going by the protocol's flag saw it green. isError is now set whenever stoppedAt is.

    • String arguments are checked against the schema. capabilities with { query: 42 } answered "query.toLowerCase is not a function"; every argument a tool's schema declares a string is now refused by name when it is not one, once, before any handler runs.

    • Three wrong pointers. generation_rules linked docs/cookbook/overview, a page the site never had (now the cookbook's index; a test checks every docs link against a local docs checkout); scaffold_app's schema advertised ^[zy]c[lx]_ while the template enforces ^z(cl|cx)_; and run_unit_tests' class_names errors showed the example ["sap.m.Wizard"], which is scope_of's.

    • validate_view keeps the property findings when the render gate cannot start. Without @abap2ui5/linter-render, or with a Chromium that will not launch, the linter throws, and the tool returned that throw - an error or 5 KB of Playwright's ANSI launch log - instead of the findings the property gate had already computed. It now repeats the check with render: false, says why in notes and renderSkipped, and answers.

    • The project's own linter is found. app-template ships @abap2ui5/linter as a devDependency, yet the server started in such a project said "linter checkout not found - clone it as a sibling". After the sibling checkouts, <cwd>/node_modules/@abap2ui5/linter and the server's own node_modules are candidates now; the missing-linter message and the README's Level 1 name all the ways in, including npx -p @abap2ui5/mcp-server -p @abap2ui5/linter -p @abap2ui5/linter-render abap2ui5-mcp. The README's "~3 MB" for Level 1 was the linter without its render runtime; with it (the @openui5 libraries and Playwright) the install is ~150-200 MB, and it now says so.

    • The framework clone is the release, not its 7.02 downport. Without a checkout, build_backend and an unpinned abap2ui5-unit cloned the tag GitHub's releases/latest names - and the framework publishes every version twice, X.Y.Z and seconds later X.Y.Z-702, so "latest" was the downport (downported sources, no prebuilt backend). The release list is read instead and the highest plain X.Y.Z taken.

    • scaffold_app escapes what it writes. The package text and the repository name were spliced into the XML sidecars and package.json raw, as a String.replace replacement string: R&D <tools> produced unparseable XML, my"repo an unparseable package.json, and $& was expanded into the matched text. Both are now escaped for the format they land in and substituted through replacer functions.

    • The mirror stays inside its directory. The template mirror joined every path template.json lists (and the docs mirror every path the tree listing names) onto the cache directory unchecked, so an entry like ../../x was fetched and written outside it. Listed paths now pass the same whitelist as an agent's path; one bad entry refuses the mirror.

    • A stale GITHUB_TOKEN no longer breaks the GitHub mirror. The token was sent to raw.githubusercontent.com too, which answers a token it does not accept with 404 - every knowledge tool then reported every file as missing. It now goes to api.github.com only, and a token the API refuses (401/403) is dropped with one warning on stderr and the request repeated without it. A used-up unauthenticated API limit (the docs tree listing) is reported as that, with the token as the remedy, instead of a bare HTTP 403.

    • run_app boots with the corpus' local UI5, and honours timeout_ms. The framework page's hash-only CSP blocks the inline scripts the SOURCE sap-ui-core.js from the local @openui5 packages document.write()s, so with a samples-controls checkout beside it no app ever booted; the browser context now bypasses CSP exactly when local sources are served (with the CDN the page keeps its own policy). The boot wait passed { timeout } as waitForFunction's page argument instead of its options, so timeout_ms (run_app, interact_app, verify_app) was ignored and every boot waited Playwright's default 30 s.

    • The second dev app lints. deploy_app without a description wrote the constant MCP dev app into every sidecar, and abaplint's identical_descriptions (on in app-template's config, which lints the framework sandbox) failed every app after the first - verify_app stopped at deploy. The default now names the class.

    • Removed apps leave the build. The incremental build copied the sandbox into the framework's node/downport and never took anything out: after remove_app the class was still transpiled (a broken one kept failing every build_backend), a redeploy without testclasses kept the old test include running, and abap2ui5-unit left every class it tested in a developer's checkout. The copies are now tracked in a manifest inside node/downport (.abap2ui5-mcp-dev-copies.json) and removed once their sandbox source is gone - by the build and by remove_app itself; the framework's own files there are never touched. The sandbox's package.devc.xml is no longer copied over the framework's.

    • Security: no registry fallback for abaplint and the transpiler. The lint ran npx abaplint and the incremental build npx abap_transpile; in a checkout without its own install npx did not fail but, with no TTY to prompt on, installed whatever the registry has under that name - and abap_transpile is an unclaimed name there (dependency confusion). Both now run the checkout's own node_modules/@abaplint/cli / @abaplint/transpiler-cli bin with node, and a missing install is a message naming the checkout and npm ci.

    • npx --yes @abap2ui5/mcp-server runs the server. 0.2.0 shipped two bins, abap2ui5-mcp and abap2ui5-unit, and none named after the package, so npx stopped with "could not determine executable to run" - the command the README, the docs, the VS Code extension and app-template's .mcp.json all give. The bin mcp-server fixes that from this version on; the README now leads with npx --yes -p @abap2ui5/mcp-server abap2ui5-mcp, which 0.2.0 answers too. The release workflow runs the packed tarball through npx the way users do (scripts/pack-smoke.mjs) and moves the floating major tag v0 - which uses: abap2UI5/mcp-server@v0 names and which never existed - to each release; until then the README pins @v0.2.0.

    • The server exits with its client. It stopped only on SIGINT/SIGTERM, but a client ends a stdio session by closing the pipe, and under npx a SIGTERM reaches npm rather than the server - so node, the warm Chromium and the express backend outlived every session, and the next one's backend start met the old backend on the port. An ended or closed stdin and SIGHUP now shut down like the signals do: browsers closed, the backend stopped, every running build/lint child's process tree killed, with a five-second hard stop.

    • Dependencies: @modelcontextprotocol/sdk ^1.30.1 and playwright ^1.63.0 (lockfile refreshed within range). The linter's render runtime is now published as @abap2ui5/linter-render (formerly @abap2ui5/render-runtime); the docs and comments that name the install say so.

  2. v0.2.0

    Other 19

    Notes
    • Review round over the new tools, six fixes. build_backend mode transpile passed the schema and the build but not the server's own mode gate; verify_app never hydrated the app-template mirror its deploy stage reads, so its message named a remedy that did not work through it; the abap2ui5-unit bin did nothing when started through npm's symlink (the main guard compared the link, not the real path) and --help printed its shebang; a workspace clone on another release than the project's pin was used instead of replaced; a download that died mid-stream could turn the "never rejects" download into a rejection on Windows; and the mirror tests failed in a sibling workspace although nothing was wrong.

    • Unit tests in CI, without a system. scripts/ci-unit.mjs — shipped as the bin abap2ui5-unit and wrapped by action.yml as the composite GitHub Action abap2UI5/mcp-server@v0 — clones the framework at the release the project's abaplint.jsonc pins, gets its backend (the release asset, or the framework's own build), deploys every class under src with its test include into the framework sandbox, transpiles once and runs the tests through the generated runner filtered to those classes; a step summary names every test method and the first failure. Measured against app-template's starter app: 17 s with a built framework next door.

    • New tool verify_app. Validate, deploy, build, unit and boot in one call, stopping at the first failing stage; stages carries every result, stoppedAt the stage to read.

    • build_backend mode transpile. The framework's own build in its checkout (npm run downport + auto_transpile, a few minutes, no corpus) — what auto falls back to when the release carries no prebuilt asset yet, said in the log. run_unit_tests takes class_names for several classes in one run.

    • The whole loop with one checkout, or none. The dev sandbox has a second home: with no samples-controls checkout, deploy_app writes into the abap2UI5 checkout's node/zz_dev and lints with app-template's own abaplint.jsonc (the framework sources as the dependency — the lint a real project runs, 2-3 s), the incremental build copies from there, read_app, remove_app and run_unit_tests follow. build_backend mode prebuilt (and auto without a prior build) clones the framework's latest release into ~/.abap2ui5-mcp (A2UI5_MCP_WORKSPACE) when no checkout is there and nothing is configured. deploy_app's sidecar now carries the UTF-8 BOM abapGit writes — the template's xml_bom rule caught its absence on the first deploy into the new sandbox.

    • New tool setup_status. One read: which checkout each tool would use (local, GitHub mirror, or missing and why), the sandbox and what is deployed in it, whether the backend is built, prebuilt and running, where a framework clone would land, and whether git, tar, npx and a Chromium are there.

    • The cheap half works without a single checkout. When no local checkout resolves and no env var is set, the knowledge tools (app_guide, api_reference, pitfalls, capabilities, examples, docs_search, scaffold_app, generation_rules) and the resources read their files from GitHub into a per-user cache that the server treats as a read-only checkout (lib/remote.mjs; a day old at most, A2UI5_MCP_REMOTE=0 or A2UI5_MCP_OFFLINE=1 switch it off). A set env var stays authoritative and a failed download degrades to the old message plus the reason. The tools that write or build refuse the mirror with the clone command. lib/repo-dirs.json gained the framework's own entry (a2ui5) for the mirror URL.

    • New tool read_example. The source of a sample an examples hit named, by class or by repo + path — from the checkout, or fetched from GitHub.

    • build_backend mode prebuilt, and auto uses it first. The framework's release workflow attaches backend-<version>.tar.gz to every release; the server downloads and unpacks it into the abap2UI5 checkout in about a minute instead of the tens-of-minutes full build, and needs only that checkout for it. The incremental transpile now works on top of it (the framework's own node/deps libraries are used when they are there; the corpus-style clone and patch only otherwise). run_app no longer requires samples-controls: UI5 comes from the CDN when the corpus is not there to serve it locally. A failed download is reported, never turned into a full build.

    • New tool interact_app. Boot an app, then click, fill, press and wait through a short script and photograph the result — the event branch of main( ), which no tool without a system could reach before. The first failing action stops the script; the picture is still taken.

    • New tool run_unit_tests, and deploy_app takes testclasses. The local test classes are written beside the app (the sidecar carries WITH_UNIT_TESTS), transpiled by the next build, and run in the open-abap runtime — filtered to the one class through the generated runner, or the whole tree. remove_app and read_app know about the include.

    • Three new tools. fix_view applies the linter's mechanical fixes to a source and returns the corrected source (it writes nothing — the agent decides where it goes), reporting which findings were fixed and which remain; validate_view findings now carry fixable: true where fix_view can clear them. build_log pages through the last build's full retained output — the error a 30-line tail cut off used to cost another tens-of-minutes build to see — persisted across server restarts. read_app reads a deployed dev app back from src/zz_dev/ (same name gate as deploy/remove) and says whether the built backend already carries it.

    • Cancellation reaches the children. The MCP request's abort signal (notifications/cancelled) now kills the spawned process tree of build_backend, run_app, deploy_app's lint and scope_of — a cancelled build no longer keeps transpiling under a request nobody is waiting for.

    • Two backend-lifecycle bugs. A killed backend's late exit event cleared the NEW backend's reference (kill() is asynchronous), leaving a live express server that status denied and stop could not reach; and two concurrent starts could spawn two servers onto one port — startBackend is single-flight now, the way buildBackend already was.

    • Logging and completions declared. Diagnostics travel as MCP notifications/message once the transport is up (stderr stays the fallback), and the abap2ui5://guide/{chapter} template completes its argument from the guide's live chapter headings — advisory, so a missing checkout answers an empty list, never an error.

    • A warm renderer for the fast loop. Where the linter accepts an already-open renderer (its { renderer } option and ./render export), validate_view and screenshot_view keep one Chromium warm across calls — the cold start dominated both — keyed per theme, shared by concurrent calls (the renderer's own page pool queues them), closed on shutdown, and dropped-and-relaunched when it dies (that call falls back cold). All feature-detected: an older linter keeps exactly the cold path it had.

    • Progress from the fast loop. validate_view and screenshot_view forward the linter's onProgress phases when a progressToken is sent (an older linter simply ignores the option), and deploy_app marks its abaplint pass with start/end progress.

    • The live-read contract, made affordable. Parses are cached per file version (path, mtimeMs, size) — a pulled or edited file invalidates itself — for the docs tree, the three sample catalogues, the client interface and CAPABILITIES.md; the linter's package.json is no longer re-parsed per import, and the benign-noise list is resolved per call instead of frozen at server start, so a corpus checked out later is found without a restart.

    • Arguments that reach spawn argv are validated (scope_of entities: an array of bounded, non-empty strings, refused by name otherwise), and the prose stopped quoting sizes of sibling artifacts ("the 669-line interface") that nothing re-measures — a gate now refuses line-count claims in tool and resource descriptions.

  3. v0.1.1

    Other 8

    Notes
    • Every enumerated argument is checked, and every numeric one is bounded. The schemas declare enum and type: number; a client is free to send anything anyway, and only pitfalls and api_reference said so. The rest fell through: build_backend read args.mode || 'auto', so mode: "incremental " started a full build - tens of minutes to prove that a typo is not a mode; backend answered status for any action it did not recognise; capabilities, and examples for repo/area, filtered every entry away and reported no matches, which reads as an answer. limit: 0 returned the entire catalogue and limit: "abc" returned nothing; timeout_ms had no ceiling and a viewport could be 99999x99999. One helper (lib/args.mjs) now does both jobs, and an invalid argument comes back as a sentence naming what is accepted.

    • A sibling checkout is identified, not just probed for a file two repositories share. samples and samples-stack both probed SAMPLES.md, so SAMPLES_HOME pointed at the wrong one resolved cheerfully and answered from the wrong catalogue; the linter probed a bare package.json, so any Node project in a directory named linter resolved as the linter and failed later and elsewhere. lib/repo-dirs.json entries can now carry identity checks, which only ever rule a candidate out and are skipped where the file they read is absent - a checkout from before catalogue.json still resolves.

    • Lints no longer race over one config file. deploy_app writes .abaplint-mcp-dev.jsonc into the corpus root and deletes it in a finally; two calls at once meant the first to finish removed the config the second was still being linted against. Lints are queued now, keeping the file name the corpus gitignores.

    • The server survives an uncaught throw, logging it to stderr instead of taking the stdio session, the built backend and the browser down with it. And run_app writes its screenshot to <tmp>/abap2ui5-mcp-screenshots rather than into the install directory - which is inside node_modules for an npm install - with A2UI5_MCP_SCREENSHOT_DIR to put it somewhere you keep.

    • Gates for the rules that had none. CI clones the framework checkout, so the four tools and five resources that read it are exercised rather than skipped; the stdio smoke calls examples, the parser that has silently broken twice; the count drift gate reads spelled-out numbers and every lib/*.mjs; the ASCII rule is checked over the sources; and the release workflow refuses a tag whose changelog section is missing or whose Unreleased block is not empty. Plus tests for the scaffold substitution engine, the linter exports-map resolution and the prompt dispatch, which is a map now rather than a ternary that gave every unrecognised prompt the porting brief.

    • MCP resources and prompts, next to the tools. The server used to declare { tools: {} } and nothing else — a client that surfaces resources or prompts saw an empty server, and an agent had to learn from sixteen descriptions that app_guide comes first. The knowledge documents the tools slice are now also readable whole, under stable abap2ui5:// URIs (the app-building guide plus a guide/{chapter} template, the client API summary, CAPABILITIES.md, the porting rulebook, both pitfall catalogues) — same live reads from the sibling checkouts, listing free of any file access, and a read against a missing checkout failing with the same actionable message the tool returns (the sibling table moved to lib/siblings.mjs so there is one copy of those words). Two prompts render the workflow itself: build-an-abap2ui5-app and port-a-ui5-sample, orchestration scripts over the existing tools that duplicate none of their content. The tool-surface drift gate covers both new surfaces: README tables and counts are checked against the arrays, and every tool a rendered prompt names must exist.

    • The README installs from npm. Level 1 no longer asks for a clone of this repository — npx --yes @abap2ui5/mcp-server is the command, and a checkout is named only as what you need to work ON the server. The client-registration snippets show the same shape, with node /path/to/server.mjs as the alternative rather than the default. The npm ci install is ~45 MB and 19 MB of that is the Playwright driver only run_app imports, so the README says so where somebody first pays it.

    • npm run check — the ecosystem-wide name for "what CI will say about this tree". Here CI runs the test suite and nothing else, so it is npm test.

  4. v0.1.0

    Other 16

    Notes

    The first version on npm. Before it, the server was installable only as npx --yes github:abap2UI5/mcp-server — whatever main held that minute. Everything below is what 0.1.0 carries.

    • Renamed: the repository is mcp-server, the package is @abap2ui5/mcp-server. mcp names a protocol; mcp-server names the thing, which is what somebody scanning the organisation's repository list needs to read without clicking. The rename happened before the first publish on purpose: a package name is the one thing a release cannot take back, and a repository whose name differs from its only package is a discrepancy nobody has to inherit.

      lib/repo-dirs.json — the ecosystem's rename history — now carries an entry for this server itself, listing mcp-server and ai-mcp. It resolves nothing for its own sake, but a consumer that looks for a local checkout by directory name (abap2UI5/vscode-extension does, before falling back to npx) would otherwise miss one carrying the previous name.

    • scaffold_app: the files a new project starts from. The server could tell an agent how to write a class (app_guide) and where to put one so it could be run (deploy_app, into the corpus' scratch package) — but not how to start a REPOSITORY, which is what somebody building an app of their own actually needs. Everything around the class is the part an agent cannot invent: the abaplint config with the framework pinned at a release under the branch key, the abap2ui5lint.jsonc the render gate needs to run rather than skip, the CI workflow, the .abapgit.xml, and the .clas.xml sidecar whose CLSNAME must match the class or the object does not activate at all. Served live from abap2UI5/app-template, the repository this ecosystem already points people at, rather than embedded — a copy here would be a second answer to "what does a new project look like".

      class renames it throughout: the ABAP, the sidecar's CLSNAME (upper case there, lower in the source — that asymmetry is why the template ships a rename script rather than an instruction) and the file names. The name is validated before it is substituted, since it reaches file paths. Proven end to end: a scaffolded project installs and passes npm run check — abaplint 0 issues, linter 0 findings with the render gate on.

    • screenshot_view: an agent can SEE the view in seconds. The linter gained --screenshot — it reconstructs the view from the builder calls, seeds it from the class's own TYPES/DATA and photographs it in the same headless harness its render gate already runs — and this server had no way to reach it. The only way to look at anything was run_app, which boots the REAL app and therefore needs the whole framework transpiled first: tens of minutes for the first build, minutes for every rebuild after an edit. So the loop was "write ABAP, get a verdict in seconds, then pay a build to look at it, or never look at it". A dedicated tool rather than a flag on validate_view, because it is a different question (is this legal / what does it look like), it takes different arguments (viewports, theme, preview data) and it needs the render runtime and a browser, which the property gate does not. Several viewports come back from ONE browser session, each as an MCP image block — the way run_app has always returned its screenshot.

    • app_guide: the rulebook for the job this server is for. The one rulebook on offer, generation_rules, serves samples-controls' generation-prompt.txt, whose first line is "You are porting one official UI5 demo kit sample to abap2UI5" — while the tool described itself as "the canonical rulebook for writing an abap2UI5 app". An agent building a user's app was being handed the porting brief: an input sample it does not have, a z2ui5_cl_smpc_app_<n> convention that is not its app's, and 1:1 fidelity to something that does not exist. abap2UI5 maintains the right document beside its sources (docs/agents/building-apps.md, deliberately self-contained so no web access is needed); it is served live and sliced by chapter, the way pitfalls slices the skills. The porting brief stays where it was, and both descriptions now say which job they are for.

    • An agent can deploy the app it actually wrote. deploy_app enforced ^z2ui5_cl_[a-z0-9_]+$ — the naming convention of the demo-kit PORTS — and the ecosystem's own starting point, abap2UI5/app-template, ships zcl_app_001. So an agent that followed the recommended path could not deploy, build or look at the thing it had just been told to write. Any customer-namespace class name is accepted now (^[zy][a-z0-9_]*$, <= 30 chars), and the dev lint config was widened the same way — it forced ^Z2UI5_CL_ one layer down, which would have failed the very name this server had just accepted. The safety property is unchanged and tested: the name becomes a PATH under src/zz_dev, so it is still a whitelist admitting no separator, dot or space, and no name can reach outside the sandbox.

    • A finding arrives explained. validate_view returned a rule id and a one-line message; the paragraph saying why the defect matters and what the fix looks like existed only on the published rules page — a web fetch mid-task, and one an agent may not be able to make at all. The linter now exports that prose (./rule-docs), and each rule that fired comes back under rules, keyed by id so twelve findings of one type cost one explanation. The one-line summary always, the full paragraph on explain: true — a first run on an unfamiliar class can hit a dozen distinct rules, and a dozen paragraphs would crowd out the findings they are about. An older linter checkout without that export costs the explanations and nothing else.

    • The catalogue rows are read whole. Two things the parser dropped on the floor: the per-sample docs: links — the cookbook chapters somebody decided each app is the worked example of, which it knew about only well enough to SKIP while looking for the keywords — and, worse, the TITLE of every port in samples-controls. Its rows carry the whole header in bold with no dash after it (| **sap.m.Bar**<br>…) and the row pattern required the dash, so 430 of the 614 apps parsed as rows with no header at all: the title fell back to the section, and every port announced itself as the LIBRARY it belongs to while the control an agent asked for survived only inside the keyword blob. The docs links are searchable by nobody on purpose — almost every row in samples carries one starting cookbook/, so a query for "cookbook" would match the whole catalogue.

    • examples searches all three sample repositories, not one. The tool read abap2UI5/samples and nothing else, so two thirds of the answer was invisible to it: samples-controls (430 ports of the UI5 demo kit — the answer to "how do I express sap.m.Wizard") and samples-stack (32 apps that need an OData service, RAP, APC or the launchpad, which is exactly what an agent must know before proposing one). 152 apps searchable, 614 now. Each entry names its repo, and a new repo filter narrows to one. A repository that is not checked out is REPORTED rather than fatal — a thinner answer to "has somebody built this" beats a refusal — and only all three missing is an error. This became possible because the three catalogues now render the identical row from the same two lines on the class (" @summary, " @keywords), so one parser reads all of them.

    • The row parser reads the summary sentence, and could not have. The catalogues grew a second kind of block under the row title — the sentence, in normal type rather than in <sub> — and the old pattern matched <br><sub> blocks only. It would have matched no rows at all, and that failure looks like "there are no samples for that" rather than like a parse error. The blocks are matched as a group and classified afterwards, which is the same fix the @docs links needed, and the tests now cover both kinds.

    • validate_view judges a source by its own project's config. It read samples-controls' abap2ui5lint.jsonc unconditionally, which is right when porting demo-kit samples and wrong for everyone else: an app in another repository was measured against that corpus' rule overrides, allow list and UI5 floor, with no argument to say otherwise — while the tool's own description promised the opposite. New project_dir argument; without it, the working directory, then the corpus. A named project is taken at its word: its config or none, never a silent fallback onto someone else's.

    • stripJsonc deleted the wrong character. Trailing-comma offsets were collected in UTF-16 code units and dropped by code point, so one astral character — an emoji in a description is enough — shifted every later index and left unparseable JSON behind. It reads abaplint.jsonc out of a repository this server does not own, so the input was never ours to constrain.

    • The dev lint config is removed again. devLintConfig( ) writes .abaplint-mcp-dev.jsonc into the ROOT of the samples-controls checkout on every lint (it has to — the config's files glob resolves from there) and left it behind. That it never showed up in a commit rested on one line in another repository's .gitignore.

    • Two live reads say what is missing. A checkout can be present and a file absent — an older revision, a half-finished pull, a rename upstream — and generation_rules and capabilities answered that with a raw ENOENT stack trace. They now name the file and say git pull, as pitfalls already did.

    • Setup is documented in three levels, because the tools do not all cost the same: validating views needs one 3 MB checkout, the catalogues need two more, and the screenshot loop needs a browser and a first build that takes tens of minutes. Registering the server was documented for claude mcp add alone while the first paragraph promised Cursor, VS Code and any MCP client; there is a plain mcp.json block for those now.

    • CI clones samples-controls, not ai-demokit — that repository was renamed, and the clone worked only through GitHub's redirect.

    • The release proves the tarball, not just the working tree. npm test runs where every file exists whether or not files lists it, so the one defect this package can ship — a lib/ module left out of the allowlist — was invisible to the entire suite, and the release job only printed the tarball contents. It now installs the tarball into a scratch project and drives the installed bin over stdio: initialize, tools/list, and one tool call with every checkout absent, which has to come back as the actionable message rather than a crash.

34downloads per week, npm
Weekly downloads for @abap2ui5/mcp-server26 complete calendar weeks from 2026-04-20 to 2026-10-18; the latest total is 34.20 Apr – 26 Apr: 027 Apr – 3 May: 04 May – 10 May: 011 May – 17 May: 018 May – 24 May: 025 May – 31 May: 01 Jun – 7 Jun: 08 Jun – 14 Jun: 015 Jun – 21 Jun: 022 Jun – 28 Jun: 029 Jun – 5 Jul: 06 Jul – 12 Jul: 013 Jul – 19 Jul: 020 Jul – 26 Jul: 027 Jul – 2 Aug: 03 Aug – 9 Aug: 010 Aug – 16 Aug: 017 Aug – 23 Aug: 16024 Aug – 30 Aug: 2331 Aug – 6 Sept: 1717 Sept – 13 Sept: 1514 Sept – 20 Sept: 5221 Sept – 27 Sept: 17028 Sept – 4 Oct: 2435 Oct – 11 Oct: 2412 Oct – 18 Oct: 34

Others providing67

Agent access to ABAP code
  1. arc-1-bopf-enharc-1 extension for reading and maintaining BOPF enhancements
  2. HammerNative macOS code editor for ABAP and CDS
  3. arc-1-lspEdition of ARC-1 that runs ADT calls through adt-ls
  4. arc-1MCP server over the ADT REST API for ABAP objects
  5. arc1-abap-bridgeVS Code bridge between arc-1 and ADT for VS Code
  6. arc1-adt-abap-mcp-extEclipse add-on enabling the ADT MCP server with read-only tools
  7. ABAP Accelerator MCP ServerAWS's ABAP MCP server for Amazon Q Developer and Kiro
  8. Modernization Agents for SAP on AWSAWS agents for ABAP, PI/PO and BW modernisation work
  9. @mcp-abap-adt/coreABAP ADT MCP server published as an npm scope and plugin
  10. chandrashekhar-mahajan/abap-mcp-serverABAP MCP server with ADT tools and five authentication strategies
  11. dassian-adtFork of mcp-abap-abap-adt-api with 25 tools and session recovery
  12. erpl-adtC++ binary speaking the ADT REST API as a CLI and MCP server
  13. bw-modeling-mcpMCP server for BW/4HANA modelling objects
  14. EPOD ADT MCP ServerMCP server for ABAP on the ADT session, in Eclipse or standalone
  15. ZARC1_EXTENSION_FORMSarc-1 extension for SAPscript and Smart Forms objects
  16. fgalastri/MCP_ABAPValidates an ABAP class before it is deployed by converting the global class to a local one and syntax-checking that — plus CDS DDL and table metadata retrieval.
  17. fr0ster/mcp-abap-adtABAP MCP server with full CRUD on on-premise and ABAP Cloud
  18. abapsmithABAP MCP server over ADT with a local journal that can undo writes
  19. sap-ai-dev-toolkitMCP proxy pinning an agent's ABAP tools to one SAP system
  20. aibap.mcpGo MCP server that writes, activates and runs ABAP classes via ADT
  21. BellaEclipse plugin bringing Claude into ABAP Development Tools
  22. Maherd18/sap-mcp-serverPolicy-checked MCP server for ABAP development over ADT
  23. sapdumpmcpMCP server reading ABAP short dumps over ADT
  24. ABAP remote filesystemVS Code extension editing ABAP systems over ADT as a filesystem
  25. mcp-abap-abap-adt-apiWrite-oriented MCP server over abap-adt-api
  26. mcp-abap-adtRead-oriented MCP server fetching ABAP programs, classes, tables
  27. SAP MCP Connection ManagerWindows desktop manager handing SAP connections to assistants over MCP
  28. ABAPilotMCP connector for ECC and on-premise S/4HANA with a licensed SAP side
  29. vibing-steampunkADT-to-MCP bridge in Go reaching past what ADT exposes
  30. ADT MCP ServerLocal HTTP MCP server started from ABAP Development Tools for VS Code
  31. ABAP Development Tools for VS CodeSAP's ABAP tools in VS Code for editing, debugging, ATC and transports
  32. SAP Joule for Developers, ABAP AI capabilitiesSAP's AI for ABAP development in ADT for Eclipse and VS Code
  33. ABAPDocMCPMCP server turning a transport request into a WRICEF specification
  34. claude-sap-kitClaude Code plugin adding Custom_* tools to arc-1
  35. Skybuffer AI MCP serverABAP add-on serving business data and transactions as MCP tools
  36. abap-adt-cliCLI that pulls and pushes ABAP packages as local files over ADT
  37. workskong/mcp-abap-adtLight MCP adapter for ABAP over ADT, documented in Korean
  38. KlystronMCP server in ABAP inside the application server, with agent skills
  39. pyrfc-mcpMCP server calling RFC-enabled function modules behind an allowlist
  40. ABAP-AI-CODE-TOOLSabapGit package adding create, modify and delete tools to ABAP AI Code
  41. ABAP AI CodeAgentic coding assistant in ABAP that runs as an SAP GUI report
  42. VERTEXADT extension for Eclipse and VS Code showing three SAP GUI tools
Unit testing
  1. abap2UI5 headless frontendDrives abap2UI5 app classes from ABAP Unit tests
  2. assertFluent assertion helper for ABAP Unit tests
  3. @abaplint/transpilerTranspiles ABAP to JavaScript to run on Node.js
  4. Modernization Agents for SAP on AWSAWS agents for ABAP, PI/PO and BW modernisation work
  5. abap_db_preparatorReplays stored database snapshots in ABAP unit tests
  6. cacamber-BDD-for-ABAPBDD framework turning Given/When/Then into ABAP Unit steps
  7. abap-utesthelpHelper classes for ABAP Unit tests and test doubles
  8. regression_testSnapshot testing library for ABAP, from debugger or unit test
  9. abapCovUploads ABAP Unit code coverage to codecov.io
  10. ABAP remote filesystemVS Code extension editing ABAP systems over ADT as a filesystem
  11. open-steamgateLocal ABAP server serving /IWBEP/ OData v2 without an SAP system
  12. express-icf-shimShim running an ABAP ICF handler behind an Express 4.x server
  13. open-abap-coreCore ABAP artifacts for code transpiled by @abaplint/transpiler
  14. mockup_loaderLoader of Excel-prepared ABAP unit test data, with stub generation
  15. mockAA free mocking framework for ABAP, published under the Apache License 2.0.
  16. Vise_for_ABAPA characterisation-test helper: `ZCL_VISE=>GRIP` records a field's value once and fails a later run if it has changed.
  17. abap-senchaBDD/TDD assertion library for ABAP Unit, styled after Chai.js
  18. abap-test-containerGitHub-hosted test data container for ABAP Cloud
  19. abap-unit-runnerApplication job that runs and schedules ABAP Unit tests in the cloud
  20. adash-servicesBackend exposing ABAP Unit runs and coverage over REST and OData
Agent access to your systems
  1. abap2UI5 for VS CodeVS Code extension to preview, activate and lint abap2UI5 apps
  2. abapsmithABAP MCP server over ADT with a local journal that can undo writes
  3. btp-sap-odata-to-mcp-serverExposes S/4HANA or ECC OData services as MCP tools
  4. midasol/sap-mcp-serverPython MCP server for SAP Gateway OData operations
  5. odata_mcp_goGo binary bridging any OData v2 or v4 service to MCP
  6. odata_mcpPython bridge generating MCP tools from OData v2 metadata
  7. sap-mcp-serverReads SAP ECC and S/4HANA for an agent over Gateway OData and RFC, with a Node bridge that speaks the RFC protocol without the proprietary SAP NetWeaver RFC SDK.
  8. Skybuffer AI MCP serverABAP add-on serving business data and transactions as MCP tools
  9. sap-security-mcpRead-only security analysis MCP server for SAP audits and SoD review
  10. KlystronMCP server in ABAP inside the application server, with agent skills
  11. pyrfc-mcpMCP server calling RFC-enabled function modules behind an allowlist

Details

Sources4
GitHub APIdiscovered herechecked
npm registry, @abap2ui5/mcp-serverversion, releases, installschecked
github.com/git+https://github.com/abap2UI5/mcp-server.gittree, pushes, contributors, CIchecked
README.mdquoted, and what the entry declareschecked

Sources are graph entities: the same page may back several entries, and an unreachable source flags every row that rests on it.

Compatibility and provenance
runtimesnode
latestv1.0.0
first release
releases and cadence5, ~14d median
discovered viaGitHub API
first seen in the atlas
ecosystem and domainabap, ai-and-agents
History in the atlas1
  1. First seen.
For your agent
Ref
artifact:abap2ui5-mcp-server
JSON
/artifacts/abap2ui5-mcp-server.json
MCP
Connecting an agent