4D LSP
Scope
This skill validates 4D source code (.4dm files) using the 4D compiler
via tool4d-lsp-stdio.
Use this skill after generating or modifying .4dm files. The validation
gives compiler-grade feedback from the real 4D engine -- do not rely on
pattern matching or guesswork to verify 4D code.
When to use
- Before writing an unfamiliar 4D command, to look up its correct syntax and a compiler-verified example (see "Command lookup" below)
- After generating a new
.4dmmethod or class - After modifying existing
.4dmcode
When to skip
- Reading code for understanding (no writes)
- Trivial changes: renaming files, moving methods, updating comments only
Tool location
Prefer tools/4dlsp/tool4d-lsp-stdio over any system-installed copy. If
tools/4dlsp/tool4d-lsp-stdio does not exist, provision it first by reading
skills/4dtools/SKILL.md.
test -x tools/4dlsp/tool4d-lsp-stdio
On Windows, check for tools\4dlsp\tool4d-lsp-stdio.exe.
Prerequisites
tool4d-lsp-stdio requires tool4d (the headless 4D runtime). It
searches for tool4d automatically in this order:
--toolargument orTOOL4D_PATHenvironment variable- System PATH
- VS Code 4D Analyzer extension storage
- macOS:
~/Library/Application Support/Code/User/globalStorage/4d.4d-analyzer/tool4d/ - Windows:
%APPDATA%/Code/User/globalStorage/4d.4d-analyzer/tool4d/ - Linux:
~/.config/Code/User/globalStorage/4d.4d-analyzer/tool4d/
- macOS:
- Conventional application locations
- macOS:
/Applications/4D*.appand~/Applications/4D*.app - Windows:
%ProgramFiles%\4D\<version>\tool4d\ - Linux:
/opt/4d/,/opt/4D*/,/usr/local/bin/
- macOS:
If tool4d is installed via the 4D Analyzer VS Code extension, no
configuration is needed. Otherwise pass --tool <path-to-tool4d> (or set
TOOL4D_PATH) explicitly -- e.g. on macOS a manually-installed 4D.app is
common and won't be found without this, since it isn't on PATH and isn't
in the VS Code extension storage or conventional locations list above.
Command lookup (before writing code)
Before writing or fixing a call to an unfamiliar 4D command or OOP class
member, look it up with the 4dlang skill (skills/4dlang/SKILL.md)
instead of guessing at its syntax from training data. It wraps
deterministic, offline references over compiler-verified 4D command and
class IRs and returns real overload signatures plus a tool4d-verified
example. This is a lookup aid for getting syntax right the first time
-- it is not a substitute for validate/check-syntax below. Always
still validate the code you write; do not treat a lookup result alone as
proof the code compiles.
Validate command
Use the validate subcommand to check .4dm files in a single call:
tools/4dlsp/tool4d-lsp-stdio validate --workspace Project/ Sources/Methods/myMethod.4dm
Or point directly at the .4DProject file:
tools/4dlsp/tool4d-lsp-stdio validate \
--project Project/MyApp.4DProject \
Sources/Methods/myMethod.4dm
Multiple files
Pass multiple file paths to validate them all in one session:
tools/4dlsp/tool4d-lsp-stdio validate --workspace Project/ \
Sources/Methods/method1.4dm \
Sources/Methods/method2.4dm \
Sources/Classes/MyClass.4dm
This starts tool4d once and validates all files before shutting down.
Output format
Human-readable output (default):
Sources/Methods/myMethod.4dm:6:1: error: Unknown command: ALRT
Sources/Methods/myMethod.4dm:10:5: warning: Variable not declared
Format: file:line:col: severity: message
JSON output with --json:
tools/4dlsp/tool4d-lsp-stdio validate --json --workspace Project/ Sources/Methods/myMethod.4dm
Exit codes
0-- no errors (warnings are allowed)1-- one or more errors (severity: error)
Interpreting results
- error -- code will not compile. Must fix before reporting success.
- warning -- code compiles but may have issues. Report to user but do not block on warnings.
- info / hint -- informational. Ignore unless relevant to the task.
Check-syntax command (project-wide)
validate requires a specific list of files. Use the check-syntax
subcommand instead when you want a project-wide compile-check pass
without knowing or listing every .4dm file yourself -- e.g. after a
refactor that may have touched files you didn't explicitly edit, or when
asked "does the whole project still compile?".
check-syntax wraps the LSP's experimental/checkSyntax request (a
project-wide check), as opposed to validate, which pulls diagnostics
per file via textDocument/diagnostic. It accepts the same
--tool/--project/--workspace/--port/--startup-timeout/
--shutdown-timeout/--skip-onstartup/--dataless/--log-level/
--json flags as validate.
[FILES]... is optional for check-syntax (unlike validate,
where it's required):
# Project-wide check, no files needed:
tools/4dlsp/tool4d-lsp-stdio check-syntax --workspace Project/
# Anchor on specific file(s) you just edited:
tools/4dlsp/tool4d-lsp-stdio check-syntax --workspace Project/ \
Sources/Methods/myMethod.4dm
- If files are given, each is opened via
didOpenfirst (so the server has at least one known/open document as a valid anchor URI). - If omitted, the tool automatically picks the first
.4dmfile found underSources/(falling back to the project root if noSources/directory exists) and opens that as the anchor document. - Exactly one
experimental/checkSyntaxrequest is sent (not one per file), anchored at the first opened document. Per the 4D Analyzer VS Code extension's own usage of this request, the anchor document is arbitrary -- the response is project-wide regardless of which document was passed.
Output format
Non-JSON output mirrors validate's per-file got N diagnostic(s) for <path> logging style, plus a previously opened: true/false note per
file in the report -- useful because the response may include
diagnostics for files that were never explicitly didOpen'd by this
process.
--json output normalizes the upstream WorkspaceDiagnosticReport
shape to match validate --json's contract exactly:
[{"uri": "...", "diagnostics": [...]}]
(Upstream nests entries as { items: [{ uri, version, kind: "full", items: Diagnostic[] }, ...] } -- note the inner per-file diagnostics
field is itself named items upstream; this CLI renames it to
diagnostics in its own JSON output, matching validate.)
Exit codes follow the same convention as validate: 0 whether or not
diagnostics were found (as long as the session ran cleanly), nonzero
only for real transport/protocol failures.
Not yet empirically verified against a live tool4d binary: it is not yet confirmed whether the response reliably includes diagnostics for files that were never opened by this process.
check-syntaxshould report project-wide diagnostics regardless of the anchor file, but treat that as unconfirmed until tested in practice. Do not dropvalidate's per-file chunking in favor ofcheck-syntaxbased on this alone.
validate vs check-syntax
validate-- check specific files you just wrote or modified; files are required.check-syntax-- check the whole project's syntax in one pass without listing every file; files are optional and only serve as an anchor document.
Diagnostics scope
--diagnostics-scope <document|workspace> controls how much of the
project the LSP initialize request asks tool4d to diagnose. It
defaults to workspace (the prior, unconditional behavior), so existing
commands and examples above are unaffected unless you pass it
explicitly.
workspace(default) -- diagnostics/checks apply project-wide.document-- diagnostics/checks are scoped to documents this session explicitly opens (viadidOpen), narrower than workspace scope.
tools/4dlsp/tool4d-lsp-stdio check-syntax --workspace Project/ --diagnostics-scope document
This flag is available on validate, check-syntax, mcp, hover,
completion, goto-definition, document-symbols, and
install-components -- i.e. every subcommand that builds its own
initialize request. It mirrors the non-standard
initializationOptions.diagnostics.scope option the 4D Analyzer VS Code
extension sends to the LSP server.
Runtime effect not independently verified: it is confirmed that this flag correctly threads the option through to the LSP
initializerequest, matching what the real VS Code extension sends. Whetherdocumentscope actually narrows diagnostics further (or affects performance) against a live tool4d server has not been empirically verified in this environment -- treat the behavioral description above as the LSP option's intent, not a confirmed guarantee.
Install-components command
install-components wraps the custom dependency/installComponents
LSP notification -- the same one the 4D Analyzer VS Code extension's
DependencyManager sends after downloading a project's dependencies
via dependencies.json.
This is not a full dependency manager.
install-componentsdoes not fetch or download anything itself. It assumes the project's components are already present on disk -- fetched out-of-band, or by a prior IDE/VS Code session -- and just tells tool4d to (re)load them. Parsingdependencies.json, GitHub/GitLab auth, and actually downloading components are out of scope here and deferred to a future change.
tools/4dlsp/tool4d-lsp-stdio install-components --workspace Project/
Unlike validate/check-syntax, install-components takes no
[FILES]... argument -- it always targets the resolved project
(.4DProject file) as a whole, not individual source files.
Behavior
- Sends
dependency/installComponentswith{"uri": <project's .4DProject file URI>}-- the project file itself, not a source file, matching what the VS Code extension'scommands.ts(fetchProjectForCommand) sends. - Waits for the matching
dependency/installComponents/donenotification before exiting successfully. - If tool4d asks the client to re-send via
dependency/installComponents/before(a real protocol behavior confirmed inDependencyManager.ts's own notification handler), the subcommand automatically re-sendsinstallComponentsand keeps waiting -- no action needed on your part.
Flags
Accepts the same --tool/--project/--workspace/--port/
--startup-timeout/--shutdown-timeout/--skip-onstartup/
--dataless/--log-level/--diagnostics-scope/--json flags as
check-syntax/validate, plus one flag specific to this command:
--install-timeout <seconds>(default300) -- how long to wait forinstallComponents/done. Installs can take much longer than a compile check, hence the separate, longer default from--startup-timeout/--shutdown-timeout.
Output format
Non-JSON output prints a single line on success:
Project/MyApp.4DProject: components installed
--json output is a single JSON object, not an array like
validate --json/check-syntax --json -- there's only one project per
invocation, no per-file breakdown:
{"uri": "file:///.../MyApp.4DProject", "installed": true}
Exit codes
0--installComponents/donereceived within--install-timeout.- nonzero -- any transport/protocol failure, or a timeout waiting for
done.
Unlike validate/check-syntax, there is no diagnostics-found/
diagnostics-not-found duality here -- install-components either
confirms the install completed or fails outright.
Workflow
- Before writing unfamiliar 4D code, look up correct command or class
member syntax with the
4dlangskill (see "Command lookup" above) - Write or modify
.4dmfiles - Run
validateon all modified files (orcheck-syntaxfor a project-wide pass -- see above) - If errors: fix the code based on error messages, re-validate
- Repeat until exit code 0
- Report success to user
Windows
On Windows, use tools\4dlsp\tool4d-lsp-stdio.exe:
tools\4dlsp\tool4d-lsp-stdio.exe validate --workspace Project\ Sources\Methods\myMethod.4dm
Linux
On Linux, tool4d is available for CI and GitHub Codespaces. Use the same syntax as macOS:
tools/4dlsp/tool4d-lsp-stdio validate --workspace Project/ Sources/Methods/myMethod.4dm
Important notes
- The validator uses the real 4D compiler. Its diagnostics are authoritative. Do not second-guess them.
- tool4d may take a few seconds to start (it loads the project). The default startup timeout is 30 seconds.
- Validate only the files you created or modified, not the entire project.
validatereporting 0 errors does not guarantee every command is current. Some obsolete/renamed commands (e.g. old commands replaced by a newer equivalent) may not surface as a compile error. If you are unsure whether a specific command is real or current -- especially one from training data, an older 4D version, or unfamiliar code -- confirm it with the MCPhovertool (see below) rather than relying onvalidatealone.hoverreturning "No hover information available" for a command-shaped token is a strong signal it is not recognized.- For unfamiliar commands or class members, prefer looking them up with
the
4dlangskill (see "Command lookup" above) before writing code at all -- it returns the authoritative overload signature and a compiler-verified example, which is more useful upfront than discovering a syntax mistake after the fact viavalidate/hover.
MCP server
The mcp subcommand starts a persistent server that keeps a tool4d LSP
session alive so agents can make repeated calls without the ~8-second
startup cost each time. It also exposes the LSP capabilities directly as
one-shot CLI subcommands (see "One-shot commands" below) -- most agent
tasks should prefer those over talking MCP/JSON-RPC directly.
Note: this section describes the design agreed in https://github.com/miyako/skills/issues/27 -- check
tools/4dlsp/tool4d-lsp-stdio --versionand--helpto confirm which of these subcommands/flags are present in your provisioned build before relying on them. Older builds only havemcp(stdio, foreground) andvalidate; fall back to "No MCP client available" below ifhoveretc. are not recognized subcommands.
One-shot commands
hover, completion, goto-definition, and document-symbols work
exactly like validate -- no persistent process, no MCP/JSON-RPC
knowledge required:
tools/4dlsp/tool4d-lsp-stdio hover --project Project/MyApp.4DProject \
Sources/Methods/myMethod.4dm --line 6 --character 21
tools/4dlsp/tool4d-lsp-stdio completion --workspace Project/ \
Sources/Methods/myMethod.4dm --line 5 --character 10
tools/4dlsp/tool4d-lsp-stdio goto-definition --workspace Project/ \
Sources/Methods/myMethod.4dm --line 5 --character 10
tools/4dlsp/tool4d-lsp-stdio document-symbols --workspace Project/ \
Sources/Methods/myMethod.4dm
--line/--character are zero-based, same as the MCP tool parameters
below. Add --json for structured output. These accept the same
--tool/--project/--workspace/--startup-timeout/etc. flags as
validate.
File path resolution differs by mode. With --project/--workspace
given explicitly (standalone mode), the file argument resolves the same
way as validate's file list -- relative to the workspace/repo root.
Once you attach to a running server by omitting both flags (see below),
the file argument instead resolves relative to your current working
directory, since there is no workspace context on that call. A
relative path that doesn't match your cwd fails fast (in well under a
second) with a generic read/parse error that looks like "file not
found" rather than "wrong path base" -- if an attached call fails
quickly and confusingly, pass an absolute file path instead of
debugging the relative one.
A relative --workspace (or --project) value can hit the same
kind of confusing failure in standalone mode too, not just the
attached-mode case above -- if a one-shot command fails to locate a file
that demonstrably exists, retry with an absolute --workspace/--project
path before assuming the command itself is broken.
Prefer these one-shot commands over hand-rolling MCP/JSON-RPC. Use
hover on any command you're not fully certain is current -- validate
alone can miss obsolete/renamed commands (see Important notes above).
Locating a character position
--line/--character must point at the exact token you want to check.
Do not find this by trial and error (e.g. calling hover repeatedly
across a range of --character values until one returns real info) --
each call is a full request and, in standalone mode, an ~8-second tool4d
startup; scanning a dozen columns one call at a time multiplies both the
call count and the wall-clock time for no reason. Compute the offset
once from the file text instead, then call hover exactly once:
# 0-based line number of the line containing the token (grep -n is 1-based, subtract 1):
grep -n "Count tables" Sources/Methods/myMethod.4dm
# 0-based character offset of the token's first character on that line:
python3 -c "print(open('Sources/Methods/myMethod.4dm').readlines()[LINE].index('Count tables'))"
Then call hover/completion/goto-definition once with the resulting
--line/--character. If you're unsure of the exact spelling of a
command (e.g. it might be a substring of a longer identifier), narrow
with grep -bo on that one line, or use document-symbols first to see
what tokens 4D itself recognizes on the line, rather than sweeping
character positions with hover.
Reusing a running server (faster repeated calls)
Omit both --project and --workspace on any one-shot subcommand
(including validate) to attach to an already-running persistent server
for the current project instead of starting a new tool4d process:
tools/4dlsp/tool4d-lsp-stdio hover Sources/Methods/myMethod.4dm --line 6 --character 21
If no server is running, this fails with a clear error telling you to
start one with mcp or pass --project/--workspace to run standalone.
Use this pattern in a multi-step task (many hover/completion calls across
one session) to pay the tool4d startup cost once instead of per call.
Starting a persistent server for a task
Running mcp --project ... (or --workspace ...) directly daemonizes:
it forks into the background, binds the discoverable socket used by (2),
and prints the PID (and socket path) to stdout, then returns control.
tools/4dlsp/tool4d-lsp-stdio mcp --project Project/MyApp.4DProject
# -> pid=12345 socket=/tmp/tool4d-lsp-<hash>.sock
Note on
tool4d-lsp-stdio0.3.0: the originally-shipped 0.3.0 build of this daemonize/attach workflow was broken (tracked in https://github.com/miyako/language-4dm-nova/issues/42 -- daemonize always failed to start, and attached one-shot calls returned no hover/completion/etc info even for valid commands). A fix has been merged upstream (PR #43) andtools/4dlsp/tool4d-lsp-stdioin this workspace has been rebuilt from that fix, so the daemonize/attach pattern below now works reliably here. If you're working from atool4d-lsp-stdiobuild that predates that fix, fall back to running one-shot subcommands directly with--project/--workspaceon every call, ormcp --foreground.
Use this at the start of a multi-step .4dm task, then call one-shot
subcommands without --project/--workspace for the rest of the task.
Stop it when done:
tools/4dlsp/tool4d-lsp-stdio mcp --stop --project Project/MyApp.4DProject
# or: kill <pid>
A daemonized server also self-terminates after an idle timeout as a safety net if you forget to stop it.
mcp --stop returns before shutdown is guaranteed complete. The
worker and its tool4d child process may still be alive for a few
seconds after --stop prints its confirmation -- shutdown happens
asynchronously. If you need to verify no leftover process remains
(e.g. before starting a fresh server, or at the end of a task), poll
ps a couple of times with a short delay rather than checking
immediately once and concluding cleanup failed.
Start it once per task, and pick one flag consistently. Use either
--project or --workspace to start it (whichever you prefer), but do
not call mcp again mid-task to "restart" or switch flags -- if a
server is already running for this project, starting another is
redundant (and any in-flight attached calls may momentarily target the
wrong instance). If you're unsure whether one is already running,
either just try an attached one-shot call first (it fails clearly if
none exists, see above) or call mcp --stop before starting a fresh
one -- don't leave multiple starts/stops interleaved with your actual
work.
For hosts with a native MCP client that want to attach to this
server's stdio directly (JSON-RPC over stdin/stdout) instead of a
detached background process, pass --foreground to keep the original
(pre-daemonizing) behavior:
tools/4dlsp/tool4d-lsp-stdio mcp --foreground --workspace Project/
When to use one-shot vs a persistent server vs validate
- No
.4dmcommand verification needed -- usevalidatealone. - A few
hover/completion/goto-definitionchecks in one task -- use the one-shot subcommands directly with--project/--workspace; the per-call startup cost is fine for a handful of calls. - Many LSP checks across one task (e.g. reviewing every command in a
file) -- use the one-shot subcommands directly with
--project/--workspacefor now (see known issue above); once fixed, start a persistent server once (mcp --project ...), then call one-shot subcommands without--project/--workspacefor the rest of the task, and stop the server (mcp --stop) when done. - Host has a native MCP client already configured for this server --
use its MCP tools directly instead of shelling out to any of the
above; do not spawn a duplicate
tools/4dlsp/tool4d-lsp-stdio mcpprocess yourself in that case.
MCP protocol details (for --foreground / native MCP clients only)
The server runs on stdio using the MCP protocol (JSON-RPC 2.0), framed as
newline-delimited JSON (one JSON object per line) -- not the
Content-Length: header framing used by the LSP protocol itself.
It exposes these tools:
| Tool | Description |
|---|---|
validate |
Check .4dm files for syntax errors |
check-syntax |
Project-wide compile-check (see "Check-syntax command" above) |
completion |
Code completion at a position |
hover |
Documentation / type signature at a position |
goto_definition |
Find where a symbol is defined |
document_symbols |
List all symbols in a file |
open_file |
Open a file in the LSP session |
close_file |
Close a file from the LSP session |
Tool parameters
validate
{ "files": ["Sources/Methods/myMethod.4dm"] }
completion / hover / goto_definition
{ "file": "Sources/Methods/myMethod.4dm", "line": 5, "character": 10 }
Line and character are zero-based.
document_symbols / open_file / close_file
{ "file": "Sources/Methods/myMethod.4dm" }
No MCP client available (older builds only)
If your provisioned build doesn't have the one-shot subcommands above
(hover, completion, etc. as top-level subcommands) and your host has
no native MCP client either, drive the newline-delimited JSON-RPC
protocol directly over the mcp --foreground subprocess's stdin/stdout
(write one JSON object per line, read one JSON object per line back).
Any language works; example in Python:
proc.stdin.write(json.dumps(request) + "\n"); proc.stdin.flush()
response = json.loads(proc.stdout.readline())
Handshake before any tool call: send initialize (id +
protocolVersion, capabilities, clientInfo) → read the response →
send notifications/initialized (no id) → send tools/call requests
(e.g. {"name":"hover","arguments":{...}}).
Prefer provisioning a newer tool4d-lsp-stdio build with the one-shot
subcommands over this fallback -- see skills/4dtools/SKILL.md.
Notes
- Files must be opened (
open_fileorvalidate) beforecompletion,hover, orgoto_definitionwill return results, when using the MCP tools directly. The one-shot CLI subcommands handle this automatically. - All file paths are relative to the workspace (the
Project/directory) unless an absolute path is given.