Catalog · How the catalog works
pastree-mcp
An MCP server that gives an AI coding agent (Claude Code, or any MCP client) a semantic index of a Delphi / Object Pascal project group, built by PasTree.
- ★ 4 on GitHub
Anyone may press Interesting, signed in or not. It helps others find this project.
README
pastree-mcp
An MCP server that gives an AI coding agent (Claude Code, or any MCP client) a semantic index of a Delphi / Object Pascal project group, built by PasTree.
Without it, an agent answers "who calls TFoo.Bar" with grep: every Bar in
the tree, comments and strings included, then reading files to sort out which
hits are real, at thousands of tokens per question. With it, one call returns
exactly the uses the compiler would bind, one line each.
- Semantic, not textual. References, callers, overrides, implementations, descendants, assignments, creations and destructions come from resolved symbol identity - the same analysis that drives the pastree-lsp editor features.
- Project groups. A
.groupprojis read whole. Its projects are folded into as few analyses as their configurations allow, so shared units are parsed once, and every search runs across all of them. - Always current. Before each call, files changed on disk are re-analyzed, one module at a time, so line numbers in answers match the files the agent just edited - and the answer names them, so an edit made by someone else shows.
- The library read interface-only. The RTL, the VCL and the third-party
libraries of the installed RAD Studio are analyzed for their declarations,
not their bodies (light mode) - the own code exactly as before. A call
whose answer needs library bodies -
sourceof a library routine, the references or callers of a library method,lint- has PasTree analyze those units in full first, inside the query, and answers exactly as a full analysis would; when a question about a hub class makes the whole library cheaper, the analysis turns full for the session and the answer says so. - Built for an agent. Symbols are addressed by name (
TFoo.Bar) or by file, line and identifier - no column counting; a unit by its name or its file. Answers are compact text: relative paths, grouped by file and by the routine each row sits in, capped.
Measured on the client group (9 projects, 7,700 units, Win32 + Win64, RAD
Studio 13): the whole group loads in 3.9 s / 1.5 GB under the default policy,
or 6.5 s / 2.2 GB under the exact one (with --no-light, every library
body analyzed too: 6.9 s / 2.8 GB and 12.5 s / 4.6 GB). Typical calls take
60-500 ms.
Tools
| Tool | Answers |
|---|---|
status |
What is loaded: the server's version, PasTree's, build time, pid and log file; the compiler, the newest language constructs it compiles and those of later Delphi versions it rejects (the server's instructions say the same); members, analyses, unit counts, unit names that do not resolve, load progress |
find |
Declarations by name, qualified name or wildcard (in any part: *.Save, *Form.Save), project units first; TDerived.Member finds a member TDerived inherits; for a name the index does not hold, the Pascal files outside it that write it |
definition |
Where a symbol is declared and implemented, optionally with the source that follows |
source |
The exact text of one declaration - a routine's body, a whole type, a constant - numbered, with the comment above it; each overload of an overloaded name; lines cuts it to a piece of it by file line numbers; file + line alone gives the routine around a line. targets (here, in definition and in references): several in one call, names or file + line pieces, answered one after another |
members |
What a class, record or interface has, inherited members included, under the type declaring each - or what can be called on a variable, by its type |
references |
Every use across the group, grouped by file and by the routine or type it sits in; the form files' lines that bind it by name, under their component; also units, built-ins and conditional defines; for an overloaded name, each overload's uses, told apart; mode: files a count per file first, in the rows of the files that matter |
callers |
Who calls a routine - through the virtual method it overrides, an interface method it implements or a property it is the accessor of too, bare inherited; included, and the form lines that bind an event handler - and, with depth, who calls those; for an overloaded name, each overload's callers, told apart |
callees |
What a routine calls - the overload each call binds to, a property's getter or setter, the overrides and implementations a virtual or interface call may run - and, with depth, what those call |
impact |
What a change reaches, from git diff output or the declarations about to change: which projects of the group to build and test, the declarations touched with what they override and are called through, their callers or uses, the units a changed interface recompiles, and the calls a removed routine left behind - the form lines too: a handler's bindings, a component's lines, a removed handler still bound |
rename_plan |
The edits of a rename as line:column positions to apply yourself (nothing is written): declaration, resolved uses, implementation headers, paired parameters, a property's redeclarations, and the form lines that bind it by name - a component's own handlers carried along, a method's overrides, interface implementations and bare-inherited namesakes taken along; a unit renamed in its header, uses items, in '...' paths and .dproj entries, with the file name it needs. Refused whole for a library or read-only file, a binary form file, a uses alias; lines in uncompiled branches listed to check |
change_plan |
What a change of a routine's parameters or result must touch, before the edit: the signature lines that change together - its declaration and implementation header, its overrides and the method it overrides, the interface methods it implements and their implementations, a descendant's namesake calling it by a bare inherited;, the property it is an accessor of - and every call of any of them, by the routine it sits in. Said: the form lines binding it to an event (a changed signature builds and is called wrong at run time), the references handing it on to an event or a callback, the overloads a call may fall to, a signature a library fixes |
compile |
Builds the members a change reaches with the real compiler (MSBuild over the .dproj) into a directory of its own, and answers with the errors - each with its routine and source line - and the warnings and hints the change added. Nothing of the project is overwritten and build events are not run; the first build starts from the developer's own .dcu files. With code: a program, a unit or statements built alone with a member's compiler and settings - whether a construct compiles here, errors by the line of the code given; studio builds it with another installed RAD Studio (23.0, Delphi 12), which status lists, platform with dcc32 or dcc64; run: true runs a console program for up to 10 s and answers its output and exit code; pastree: true puts PasTree's analysis of the same code beside dcc's errors and says where they disagree. The answer names the exe |
related |
descendants, overrides, implementations, assignments, creations, destructions |
outline |
The structure of one unit with line numbers, without reading it; the unit by file or by name (unit) |
form |
The component tree of one form, merged with its ancestors' form files: each component's class, the method each event runs, the components it names - and a handler or a component a line names that does not exist; of a form class with no form file of its own, the ancestor's it loads |
diagnostics |
PasTree's semantic errors for a file or the whole group, from the files on disk now, each naming its routine |
unit_deps |
What a unit uses (resolved to files) and what uses it |
defines |
Conditional compilation as the analyzed configuration compiles it: for file + line, whether the line is compiled and each {$IFDEF}/{$IFOPT} around it with whether its branch is taken and why (the unit $DEFINE or switch at file:line, the project, the platform or dcc's default, or nothing), and the compiler options in effect there ($R+, $A8, ...) with what set them; for file alone, its lines not compiled; for name, where a symbol is defined and every directive naming it. A member of the group analyzed with another's defines is said - built as that project, the line goes the other way |
lint |
Named checks over the own units, a row per finding tagged with its rule: unused-uses (a unit in uses none of whose names is used) and uses-to-implementation (an interface uses entry used only in the implementation), judged over every configuration the unit is analyzed in. A row ends with but ... where acting on it may be wrong - the initialization or resource a removal leaves out of a program (alone, or together with the other rows), a branch not compiled naming the unit, a name bound to nothing it declares, a routine called here it declares too (dcc picks among every unit's), the bindings a move would change. A move row names the {$IF} blocks the entry is written in - move it with them. file, in and rules narrow it |
metrics |
What the code costs the build. recompile: what dcc's Make recompiles when an interface changes - a unit recompiles when a declaration it binds changes, not when a unit it lists does, and a declaration changes with one it takes in (a constant's value, a field's, a parameter's or an ancestor's type; the bodies of inline routines and generic types are part of theirs). With symbol: the units a change to that declaration recompiles, per program, each with what it binds; with file: the same for the unit's whole interface, and its declarations by reach; with neither: the own units ranked by it. cycles: the uses cycles among the own units, largest first, the shortest way round each, and the entries on it lint finds unused - without them, the cycle left |
Every tool that lists rows takes limit; its header counts every row, cut
or not, and limit: 0 answers whether and how many with no rows - "is it
used" for the price of one line.
Design and rationale of each: SPEC.md.
Setup
Requirements: Windows, a RAD Studio installation (12 or 13; its registry
library paths and RTL/VCL sources are what third-party and system units resolve
against), and PasTree
checked out next to this repository as ..\object-pascal-tree.
build.bat 37.0builds out\pastree-mcp.exe (Win64). build.bat 37.0 --test also runs the
smoke test (tests\smoke.ps1) and must end with built, smoke test passed.
pastree-mcp.dproj builds the same exe by hand - in RAD Studio, or with
msbuild pastree-mcp.dproj /p:Config=Release from a RAD Studio command
prompt - with the same search path and output directories, but no smoke test.
Claude Code
In the root of the Delphi project, add .mcp.json:
{
"mcpServers": {
"pastree": {
"command": "C:\\Repos\\pastree-mcp\\out\\pastree-mcp.exe",
"args": ["--project", "MyGroup.groupproj"]
}
}
}or register it once from that directory:
claude mcp add pastree -- C:\Repos\pastree-mcp\out\pastree-mcp.exe --project MyGroup.groupprojTo work on pastree-mcp itself with the index, point a .mcp.json in this
repository (ignored) at --project pastree-mcp.dproj: the agent gets this
server's sources and PasTree's.
Or once for every repository:
claude mcp add --scope user pastree -- C:\Repos\pastree-mcp\out\pastree-mcp.exeWithout --project the server takes the only .groupproj in its working
directory, else the only .dproj; a directory holding neither sends it up to
the parent, until the repository root (a .git directory or file). So a
session opened in a subdirectory of the project gets the project's index. A
directory with several .groupproj (or several .dproj and no .groupproj)
stops the walk - that repository needs its own .mcp.json with
--project, which overrides the user-scope registration of the same name.
The working directory is not always the session's: the Claude desktop app
starts the servers of a session in a git worktree in the main checkout. So
the server asks a client that has MCP roots (Claude Code does) for them,
and when the walk from the first root finds another project, indexes that
one (0.47.0). The load waits for the answer, 3 s at most. status says
where the server was started, the session's directory, and which project
it took; the log names the working directory too. With --project the
roots are only shown.
Where the walk finds no project, or several, the server starts without one
(0.46.0; it exited before the handshake, and Claude Code showed "Connection
closed" in every repository without a Delphi project). It indexes the
installed RTL and VCL instead - a program of its own using 34 of their
units; 0.2 s, 56 MB - so find, definition, source, members,
references, callers, callees, related and outline answer over the
library, and compile with code builds as a bare program (Win64 unless
platform or --platform, the default namespaces) with any installed
compiler; file builds a program or unit on disk the same way, relative to
the working directory. Everything is under
%TEMP%\pastree-mcp\no-project-<dir>-<hash>\: library\ (the program),
build\ (the exe of each code) and pastree-mcp.log. status says why there is
no project and that --project gives one; the other tools refuse with the
same. A repository of Pascal notes, such as the language reference, needs
nothing more to check a claim against dcc.
The tools then appear to the agent as
mcp__pastree__find and so on, together with instructions on when to prefer
them over grep. A new session is needed after registering or rebuilding -
new, not resumed: a resumed session starts the new server but keeps the tool
schemas and instructions it read from the old one, so arguments added since
(targets, code) stay invisible to the agent, although the server accepts
them.
The grep hook
Agents with the server configured often search by text anyway (SPEC 10.4:
the index was called in 5 runs of 12 when nothing mentioned it, and the one
wrong answer was a run that grepped). pastree-mcp.exe --hook is a Claude
Code PreToolUse hook that catches that moment. In the Delphi project's
.claude/settings.json, or in ~/.claude/settings.json for every
repository (it is silent where no .groupproj or .dproj is found from the
session's directory up to the repository root):
{
"hooks": {
"PreToolUse": [
{
"matcher": "^(Grep|Bash|PowerShell|mcp__pastree__.*)$",
"hooks": [
{ "type": "command", "command": "C:/Repos/pastree-mcp/out/pastree-mcp.exe --hook" }
]
}
]
}
}A search for a name - an identifier with both cases in it, TFoo,
SaveToFile - over Pascal sources (Grep with no glob or a Pascal one;
grep, rg, findstr or Select-String in a command naming a Pascal file or
searching recursively; not a search in files named one by one, which is
a read, nor one with no file that reads a pipe - a build's output), or
sed -n A,Bp of a Pascal file, is denied once with a note naming the call
that answers it (references, find, callers, source with lines -
one targets entry per range when sed reads several). The same command run again goes through:
a search for a comment or a string is the agent's to make. At most one note
per 10 minutes, and none within 5 minutes of a pastree call - the agent
knows the tools then. About 15 ms a call; it reads no project and writes
no log, only a small state file per session under
%TEMP%\pastree-mcp\hook. Any failure lets the call through.
Options
| Option | Default | |
|---|---|---|
--project <file> |
discovered | .groupproj, .dproj, .dpr or .dpk; none discovered: the installed library, no project (above) |
--also <file> |
none | A .dproj or .dpr outside the group, indexed, checked and built as a member of it (status marks it); repeats. Relative to the project's directory. For a tests project no .groupproj lists |
--studio <ver> |
$BDS, else newest |
RAD Studio registry version: 37.0, 23.0 |
--platform |
per project | Override every member's platform: Win32, Win64 |
--config <c> |
per project | Build configuration: Debug, Release |
--groups |
shared |
shared: one analysis per platform. strict: one per distinct configuration. See SPEC.md |
--light <dir> |
none | A directory tree whose units are read interface-only, besides the installed library (registry library and browsing paths, Studio sources); repeats. Relative to the project's directory. Under the group directory its units count as library, not own (SPEC 9.9) |
--no-light |
off | Analyze every library unit in full, bodies included - as before 0.31.0 |
--build-dir <dir> |
%TEMP%\pastree-mcp |
Where compile builds: a directory per group, member and configuration, kept between sessions so a build is incremental |
--log <file|none> |
beside the project | <project>-pastree-mcp.log, truncated per run. A file another server holds is not shared: this one logs to <name>-<pid>.log beside it and says so on stderr; status names the file written |
Trying tools without a client
out\pastree-mcp.exe --project X.groupproj --call find {\"query\":\"TFoo\"}
out\pastree-mcp.exe --project X.groupproj --script calls.txt--script reads one tool {json} per line (see tests\smoke.calls). The
analysis is built once, every call runs against it, and each answer is printed
with its time and approximate token count. This is the way to measure a tool
before an agent uses it.
Measuring against grep
powershell -File tests\bench.ps1 -Project X.groupproj -Bench questions.bench -Out report.mdruns a list of questions (format in tests\fixture.bench), each with the tool
call that answers it and the grep patterns and reads an agent without the
server would start with. The report sets the tokens of the answer against the
grep output with and without context lines, and sorts every grep hit: in the
answer, in a comment or string, or a namesake - and the answer rows grep did
not find. -Detail N lists those hits. The baseline is a lower bound: one pass
per pattern, no file opened to tell hits apart. A grep line searches the
Pascal sources; grep-dfm the text form files (.dfm, .fmx) with them, and
skips a binary form file, as ripgrep does.
Auditing answers over a sample
powershell -File tests\audit.ps1 -Project X.groupproj -Sample 400 -Seed 1 -Out run.jsonldraws a reproducible, stratified sample of the group's declarations (from
outline of every unit, and the library ones its code calls) and runs a
battery of calls per symbol - find, definition, references, impact, and
source, callers, callees, members, related where they apply - through one
analysis. -Forms adds form on every form file, -Symbols re-runs a
written sample, -Calls any list. A call the server dies or hangs on is
recorded as such and the server restarted after it. The output is one JSON
line per call: status, time, tokens, rows, the count the header states,
notes and the answer - the raw material for checking that answers are right,
not only what they cost. Options in the script's header.
Status
A prototype. It works end to end - including on the client group, where its answers were checked against the compiler, grep, a form-file oracle and a cold server (SPEC.md section 10) - but has not yet been used by an agent over a real task. See "Open questions" in SPEC.md.

