English | Polski
Reads PackagesLocalDirectory read-only and builds its own index (SQLite) outside the AOS repository.
Claude uses it over MCP instead of reading huge AOT XML files. Changes between versions: CHANGELOG.md.
© 2026 WillowDev. Released under the MIT License.
An independent project, inspired by Graft (a code graph for coding agents in many languages); xpp-lens shares no code with it and is built for X++ and the AOT. Until version 1.2.0 it was called xpp-graft — install.ps1 takes over an existing xpp-graft installation (settings, index, Claude registration).
The install package is not part of the repository (dist\ is excluded by .gitignore). There are two ways:
A. From a release package — nothing needs to be installed on the target machine, not even .NET.
- Download
xpp-lens-X.Y.Z.zipfrom the Releases tab of this repository. - Unzip it and run in a regular PowerShell window (not as administrator):
powershell -ExecutionPolicy Bypass -File .\xpp-lens\install.ps1 -Languages en-US,plB. From source — requires the .NET 9 SDK.
git clone https://github.com/willowdev-code/xpp-lens.git C:\Dev\xpp-lens
cd C:\Dev\xpp-lens
.\pack.ps1 # creates dist\xpp-lens\ and dist\xpp-lens-X.Y.Z.zip
powershell -ExecutionPolicy Bypass -File .\dist\xpp-lens\install.ps1 -Languages en-US,plThe installer copies the files to C:\Tools\xpp-lens, detects PackagesLocalDirectory (from the AOS web.config
or the folder layout), writes the configuration, registers the server in Claude Desktop and Claude Code, and finally
builds the index (custom models take about a minute, the Microsoft standard 20–90 minutes once, depending on the disk).
Claude may stay open: start a new session afterwards (or restart Claude Desktop) to load the server.
Updating an installation: xpplens update shows whether a newer release exists; xpplens update --install downloads
it (SHA-256 checked) and runs its installer — settings and index are kept. Running install.ps1 from a newer package
does the same. Claude does not have to be closed: an xpplens.exe used by an open session is renamed to *.old
(deleted later), so that session keeps the old version and new sessions start the new one. At the end the installer
offers to restart Claude Desktop (xpplens update --install --restart-claude or install.ps1 -RestartClaude: without
asking); in Claude Code /mcp → reconnect xpp-lens does the same for one session.
Useful parameters:
.\install.ps1 -PackagesDir K:\AosService\PackagesLocalDirectory # instead of auto-detection
.\install.ps1 -Languages en-US,de -DisplayLanguage de # label languages
.\install.ps1 -FullModels XPL,XPLRetail # force models into the full index
.\install.ps1 -StandardModels HugeIsvModel # push a model down to the standard tier
.\install.ps1 -NoStandard # custom models only (fast, small index)
.\install.ps1 -First -NoBuild -NoRegister # non-interactive / CIUninstall: uninstall.ps1 (removes the MCP entries that point to this installation, asks about the index and files).
New distribution package: pack.ps1 (add -FrameworkDependent if you prefer 5 MB and a .NET 9 requirement).
xpplens config # show settings
xpplens config --add-language de # add a label language
xpplens config --add-full-model XPL # model/package into the full index
xpplens config --add-standard-model ContosoIsv # model into the standard tier
xpplens config --add-standard-publisher "Contoso"
xpplens config --packages-dir K:\AosService\PackagesLocalDirectory
xpplens config --standard-code false # no call references of Microsoft code (smaller index)
xpplens config --usage-log false # do not record MCP calls for 'xpplens stats'
xpplens detect [--set] # detect PackagesLocalDirectory
xpplens build # apply changes
xpplens build --std-only # add the Microsoft standard to an installation made with -NoStandardSettings live in xpplens.json next to the bin folder and can also be edited by hand.
| Tier | Models | Contents |
|---|---|---|
| full | all non-Microsoft models (detected by Publisher in the descriptor) + those in extraFullModels |
objects, members, method signatures and source, code references (calls incl. chained, types, fields, intrinsics, labels), metadata references |
| standard | Microsoft models | objects, fields/indexes/relations, method signatures with line ranges, extends, CoC and event handlers, and calls from method bodies (calls, new, intrinsics — no field reads, types or labels; switch off with --standard-code false) |
| compiled | packages deployed without XML (e.g. country localizations or ISV modules shipped in compiled form only) | object names from bin\*.md, methods, fields, field groups, relations and compiler references from .xref, CoC from ChainOfCommand.xml, inheritance from ClassExtends.runtime, labels from Resources\<language>\*.resources.dll — no source code |
A standard package can be promoted to the full tier (--add-full-model ApplicationSuite) if you need every reference
there (fields, types, labels) and instant xpp_grep. The cost is a longer build and a larger database.
Index size: about 0.6 GB without standard calls, about 0.9–1 GB with them (a typical dev VM with ~190k standard files).
Compiled packages rebuild themselves when their .xref, .md or resources change. Manually:
xpplens build --compiled-only --force. Folders that cannot be indexed at all are listed by xpplens status
in the "on disk but NOT indexed" line.
By default the index goes to %LOCALAPPDATA%\xpp-lens\index\xpp.db, where the user running Claude can write.
If you put the index in the installation folder and install as administrator, Claude (running without elevation)
cannot update it — responses then show index is read-only and results freeze at the time of the build. Fix:
xpplens config --index-path "$env:LOCALAPPDATA\xpp-lens\index\xpp.db"
xpplens buildIn read-only mode xpp_method and xpp_object still return current code (they parse the file live),
but xpp_find, xpp_callers and xpp_refs use the frozen index.
FileSystemWatcheron full-tier model folders — a save in Visual Studio is visible on the next query.- File date scan at startup and every
rescanIntervalSeconds(default 5 min) — catches Get Latest from Team Explorer. - Standard: package fingerprint (descriptors +
bin\*.dll) — rebuilt only after a platform update. - New version of xpp-lens with a changed code analyzer: custom models are re-parsed once at the next start
(about a minute); the standard tier is re-indexed once in the background, package by package — an interrupted run
continues where it stopped.
xpp_statusshows the progress. - Writes are serialized with a named mutex, so Claude Desktop and Claude Code can run side by side.
What each MCP tool answers, when Claude should reach for it, and the CLI equivalent you can run yourself.
| Tool | Answers | Use it when | CLI example |
|---|---|---|---|
xpp_find |
where objects, methods, fields are (*, ?, Object.member, dotted extension names) |
you know a name or part of it | xpplens find "Cust*Invoice*; SalesLine.createLine" |
xpp_object |
skeleton of an object: properties with labels, fields, indexes, relations, data sources, control/menu tree, methods with line ranges, extensions | you need the structure, not the code | xpplens object CustTable --type table |
xpp_method |
the source of a method with file path and line range, CoC wrappers and handlers of it | you need the code; for long methods with match/lines |
xpplens method SalesTable validateWrite --match "checkFailed" --context 2 |
xpp_callers |
who calls a method — custom code first, then compiled packages and Microsoft code; chained calls (Table::find().m()) included |
impact of a change, "where is this used" | xpplens callers CustTable creditMax |
xpp_callees |
what a method uses: calls (chained receivers typed), new, fields, enums, intrinsics, labels | understanding a method without reading it | xpplens callees SalesFormLetter run |
xpp_refs |
every use of a class, table, field, EDT, enum, menu item or label | renaming, removing, finding usages of a field | xpplens refs CustTable --member CreditMax |
xpp_extensions |
CoC classes (with wrapped methods), table/form extensions, event handlers, derived classes | "what already changes this object" | xpplens ext SalesTable |
xpp_scaffold |
ready X++: CoC wrapper, table/form/data source/control event handler, delegate subscriber, pre/post handler — exact signature, naming pattern of your models | before writing an extension | xpplens scaffold coc SalesTable validateWrite --type table |
xpp_build_errors |
errors/warnings of the last Visual Studio build (model or project build), mapped to the XML file line; objects changed after the build (not compiled yet) | after a build: fix errors without pasting logs, check that everything got compiled | xpplens build-errors --severity warning |
xpp_security |
menu item / form → privileges (granted access) → duties → roles, and the reverse for privileges, duties, roles | access questions, new menu items | xpplens security CustTable --type display |
xpp_join |
shortest relation path between two tables as a ready select … join … where |
writing a query across tables | xpplens join CustInvoiceTrans CustTable |
xpp_entity |
data entity: public names, staging table, data source tree with joins, field mapping, keys; or the entities that use a table | data management / OData work | xpplens entity CustCustomerV3Entity |
xpp_changed |
objects changed on disk since a time, per model; since=build: changed after the last build |
after Get Latest, reviewing own work, before a build | xpplens changed --since 3d, xpplens changed --since build |
xpp_grep |
regex over method bodies (custom models; standard with a model filter) | text patterns the other tools cannot express | xpplens grep "ttsbegin" --model Contoso* |
xpp_label |
label id → texts in all languages, or text → existing label ids | reusing labels | xpplens label "Credit limit" |
xpp_status |
version, index tiers and counts as tables, background work | checking freshness | xpplens status (--ascii for +-| frames) |
xpp_findandxpp_objectaccept several names separated by;— one call, one section per name.xpp_methodaccepts several methods:method="insert;update"for one object, orobjectName="SalesTable.insert;CustTable::find".xpp_methodwithmatch(regex) and/orlines(120-180) returns only those lines (±context), numbered with the file line numbers; the signature and variable declarations are always included and skipped parts are marked… N line(s). Without them the whole method is returned as before.
Large trees (more than 60 elements) are collapsed to two levels with a child counter [+N]. To expand:
| Parameter | Effect | Example |
|---|---|---|
filter |
wildcard on name or path; prints full paths | xpplens object CustTable --type form --filter *PersonalTitle* |
parent |
only the subtree of one element (name or path) | --parent TabGeneral |
depth |
number of levels (below parent, if given) |
--parent UpperGroup --depth 1 |
ReferenceGroup controls show ref=<datasource>.<ReferenceField>, replGroup=<ReplacementFieldGroup>
and relPath=<DataRelationPath>. Menu extension elements show (under <Parent>), position=<PositionType>
and menuitem=<MenuItemName>.
Every MCP call is appended to %LOCALAPPDATA%\xpp-lens\usage\usage-YYYYMM.jsonl (tool, arguments, answer size,
time, empty or not). The log never leaves the machine. xpplens stats summarizes it:
xpplens stats --days 7 --top 10It shows per tool the number of calls, average / p95 / max answer size in tokens (characters / 4), time, the share of
empty answers, plus the largest and slowest calls and the latest empty answers — the places where the tool did not
help and Claude probably fell back to reading files. Switch off with xpplens config --usage-log false.
xpplens find|object|method|callers|callees|refs|ext|scaffold|build-errors|security|join|entity|changed|grep|label …
xpplens build [--full-only] [--std-only] [--compiled-only] [--force]
xpplens status [--counts] [--ascii] | stats | update [--install] | detect | config | register | unregister | mcp | version
xpplens help lists every option. Environment variables: XPPLENS_CONFIG (another configuration),
XPPLENS_VERBOSE=1 (SQL timings on stderr), XPPLENS_TIMING=1 (total time of a CLI query).
The source code lives separately from the installation, by default in C:\Dev\xpp-lens:
C:\Dev\xpp-lens\
xpp-lens.sln solution for Visual Studio 2022
src\XppLens\*.cs source
src\XppLens\Properties\launchSettings.json launch profiles (F5)
tests\XppLens.Tests\ xUnit tests + sample AOT XML (Fixtures)
build.ps1 compile; -Deploy replaces the binaries in the installation
pack.ps1 ZIP package for installing elsewhere
install.ps1 uninstall.ps1 README.md README.pl.md CHANGELOG.md LICENSE
release-notes.ps1 release notes of one version from CHANGELOG.md (used by the release workflow)
.github\ CI and release workflows, issue forms
build\ dist\ outputs (do not keep anything of your own here)
Open C:\Dev\xpp-lens\xpp-lens.sln in Visual Studio 2022 (17.12 or newer — VS 2019 does not support .NET 9).
Build with Ctrl+Shift+B. Next to the green arrow on the toolbar pick a profile from launchSettings.json
(status, stats (MCP usage), find (batch), object (form controls), method (fragment), callers (incl. standard),
scaffold coc, security, join, entity, changed (3 days), build-errors, build compiled packages,
verbose SQL (status)) and press F5 — the program starts under the debugger against the real configuration and index
(XPPLENS_CONFIG is set in the profile). Your own profile: Debug → XppLens Debug Properties → new profile,
enter a CLI command in "Command line arguments".
The MCP server cannot be debugged with F5 (it talks to Claude over stdin/stdout). To watch it at work,
deploy a Debug build (.\build.ps1 -Deploy -Configuration Debug), restart Claude and in VS use
Debug → Attach to Process → xpplens.exe.
The tests never touch your PackagesLocalDirectory or your index. They copy a small sample package set from
tests\XppLens.Tests\Fixtures\PackagesLocalDirectory (a "Microsoft" package StdBase and a custom package
ContosoCore, neutral names only) to a temporary folder, build an index there and check what the tools answer.
In Visual Studio: Test → Test Explorer (Ctrl+E, T) → Run All Tests (Ctrl+R, A). The first run builds the solution; a test can be debugged with right click → Debug.
From the command line:
cd C:\Dev\xpp-lens
dotnet test # all tests (about 10 s)
dotnet test --filter "FullyQualifiedName~QueryTests" # only the end-to-end tool tests
dotnet test --filter "Name~Scaffold" # tests whose name contains "Scaffold"
dotnet test --logger "console;verbosity=detailed" # show every test and the failure details| File | What it covers |
|---|---|
AnalyzerTests.cs |
lexer, method headers, resolved calls, chained calls (ret: chains), unresolved receivers, signatures for scaffolding |
AnalyzerTests.cs → HelperTests |
method fragments, relation info, since parsing, build result paths, batch lists, usage log and report |
QueryTests.cs |
every tool end to end on the fixture index: find (underscore, dotted names, batch), object, method (fragment, batch), callers (custom, chained, standard), refs, callees, extensions, scaffold, security, join, entity, changed, build errors, labels |
IndexFixture.cs |
builds the temporary index once for all QueryTests |
MigrationTests.cs |
taking over an xpp-graft installation (moved, custom location, locked, already there, second run) and xpplens update (versions, release JSON, SHA-256, pending notice) |
CommandLineTests.cs |
options with one or two dashes, --name=value, options before arguments, unknown options and missing values as errors, -- |
Adding a test: put the XML the case needs into Fixtures (keep names neutral — Demo*, Contoso*), then add a
[Fact] to QueryTests.cs that calls the query and asserts on the text. Run the tests before every commit.
- Set
<Version>insrc\XppLens\XppLens.csprojand add its section## X.Y.Z — datetoCHANGELOG.md. - Commit, push to
mainand wait for the green CI. - Tag and push the tag:
git tag vX.Y.Z
git push origin vX.Y.ZThe Release workflow then runs the tests, checks that the tag matches <Version>, builds xpp-lens-X.Y.Z.zip
with pack.ps1 and publishes the release with its notes from CHANGELOG.md (preview them with
.\release-notes.ps1 -Version X.Y.Z). Release tags are protected: a published version cannot be changed — fixes
go into a new version.
| Change | File |
|---|---|
| new MCP tool | McpTools.cs (declaration) + Queries*.cs (query) + CLI command in Program.cs + a test |
| other data from XML (new object type, property, member) | XmlObjectParser.cs |
| recognizing X++ constructs (calls, chains, attributes, intrinsics) | CodeAnalyzer.cs, XppLexer.cs — bump Indexer.AnalyzerVersion |
| method fragments | Fragments.cs |
| new table or index in the database | Store.cs — EnsureExtras for in-place changes, SchemaVersion only when a rebuild is unavoidable |
| refresh, watcher, model tiers | IndexService.cs, Indexer.cs, Catalog.cs |
packages without XML (.xref, bin\*.md, label resources) |
BinaryPackage.cs |
usage log and stats |
Usage.cs |
xpplens update, taking over xpp-graft |
Updater.cs, Migration.cs |
| release automation | .github\workflows\release.yml, release-notes.ps1, pack.ps1 |
| CLI commands, configuration, registration in Claude | Program.cs, Config.cs, Detect.cs |
Work loop:
.\build.ps1 # compile into .\build
.\build\xpplens.exe find CustTable # test from the command line, no Claude restart needed
dotnet test # run the tests
.\build.ps1 -Deploy # replace the installed binaries (stops running processes)
.\build.ps1 -Test -Deploy # the same, but only when all tests passAfter -Deploy restart Claude Desktop and Claude Code sessions — MCP loads the binary at startup.
Changing Store.SchemaVersion drops the index and requires xpplens build.
Documentation is kept in two languages: any change to README.md must be mirrored in README.pl.md in the same commit.
Bugs and ideas: open an issue (the form asks for the version and what you ran). Security problems: Security → Report a vulnerability, see SECURITY.md. Please never paste code, object names or data from your own or your customers' projects — reproduce the problem with standard objects or neutral names.
- Receiver types come from variable declarations and method return types (chains); there is no full type inference —
e.g. elements of containers,
this.field.method()through a field of another class, or results ofas/casts in expressions stay "receiver type unknown". - Microsoft code keeps calls only (no field reads, types, labels); for those, promote the package to the full tier or use
xpp_grep --standardwith a model filter (reads XML from disk: seconds for a small package, minutes forApplicationSuite, refused above 60k files). - Macros (
#name) are not expanded. A label is recognized only when a string holds just the label id. - Compiled packages: no source code; field group contents and object properties are not reconstructed
(only the header of
bin\*.mdis read), and references cover only what the compiler recorded. xpp_joinfollows table relations only (not EDT relations);xpp_changeddoes not list deleted objects.xpp_build_errorsmaps compiler lines to the XML file for methods of classes, tables and forms; for methods of form data sources and controls it shows where the method starts.- Windows and x64 (self-contained package); the index is not portable between machines — it is built locally.