Skip to content
Developer Tools

Language and Debug Servers

3forge Enterprise embeds 2 IDE-integration servers directly in the AmiCore JVM. They are consumed by the VS Code extension (and any other LSP/DAP-capable editor), turning a running AmiCore instance into a full language-and-debug experience.

Server Protocol Gives you
LSP Language Server Protocol Completion, hover, signature help, diagnostics, go-to-definition, find-references, rename, and formatting for AmiScript and AMISQL
DAP Debug Adapter Protocol Step-debugging of live AmiScript callbacks (Web) and Center targets (timers, triggers, procedures, methods) from your editor

Both are served over AmiCore's shared RPC WebSocket server on ami.rpc.ws.port, the LSP on the /lsp path and the DAP on /dap. Because the servers run inside AmiCore, the language intelligence and the debugger are backed by AmiCore's own parser and runtime, so what the IDE tells you matches what the deployment actually does.

Enabling the WebSocket Endpoints

Both servers are WebSocket endpoint plugins, listed in ami.rpc.ws.plugin.classes on the component. The shared server opens one port (ami.rpc.ws.port, default 33334) and hosts whichever endpoints are listed.

The shipped defaults.properties enables all 3 endpoints, so a stock deployment already serves them:

1
2
3
4
5
# The shipped default:
ami.rpc.ws.plugin.classes = DAP,LSP,LSP_OPEN

ami.rpc.ws.port        = 33334
ami.rpc.ws.bindAddress = loopback
Property Default Purpose
ami.rpc.ws.plugin.classes DAP,LSP,LSP_OPEN Which WebSocket endpoints load. Clearing it entirely stops the server from starting at all
ami.rpc.ws.port 33334 Port for the shared WebSocket server (hosts all enabled endpoints)
ami.rpc.ws.bindAddress loopback loopback/localhost (default), or any/0.0.0.0/* for all interfaces, or a specific host
ami.rpc.ws.auth.plugin.class (component default) Authenticator used for WebSocket connections

The default bind address is loopback, so the endpoints are reachable only from the same host until you deliberately expose them.

These are the AmiCore-side property names

The VS Code extension has its own client setting for the same port, spelled ami.websocket.port. It is a client setting, not an AmiCore property. Configure the server with ami.rpc.ws.* and the editor with ami.websocket.port.

For Center-target debugging, the Center additionally needs its debugger enabled:

ami.db.debug.enabled = true

A typical development deployment enables the AI, language, and debug tooling together:

1
2
3
4
ami.rest.plugin.classes   = *,!QUERY,!ADMIN     # MCP endpoint (see AI docs)
ami.rpc.ws.plugin.classes = DAP,LSP             # drop LSP_OPEN from the default
ami.rpc.ws.port           = 33334
ami.db.debug.enabled      = true

The LSP Server

What It Provides

Capability Behavior
Completion Triggered on . and :. Member methods, global functions, AmiScript keywords, and DDL OFTYPE / USE slot keywords. Receiver types are inferred from editor scope.
Hover Markdown documentation for the symbol under the cursor.
Signature help Triggered on ( and ,; shows the parameter list of the call being typed.
Diagnostics Errors and warnings on open, change, and save, produced by AmiCore's own compiler plus AMISQL idiom validation.
Go-to-definition In-document local declarations, plus workspace symbols (tables, columns, named SQL objects, script symbols).
Find references / rename Workspace-wide, backed by the AmiCore cross-reference index. Locals resolve per binding, not per spelling.
Formatting Canonical AmiScript formatting, using the same formatter as the Web editor.

The language intelligence is not a separate grammar: diagnostics run AmiCore's own parser, and completion is seeded from AmiCore's real member-method and function registries. DDL and USE-option knowledge is pulled from the live Center. In editor-aware mode, a callback is validated with its full context (its layout, this, and linked variables) so diagnostics reflect the real callback environment rather than a bare snippet.

What Gets Validated

A file is classified by what it actually is before it is parsed, because .ami is overloaded and names both AmiScript and layout JSON. Each construct is then checked in its own scope rather than as one flat buffer:

  • .amisql method bodies are compiled individually, with the method's declared parameters in scope, and calls to other methods declared in the same file resolve.
  • script="""…""" trigger and timer bodies are compiled as bodies, and may also call methods declared in the same file.
  • Layout callback bodies inside a .ami layout are extracted and validated. A callback's linkedVariables supply the names it may use. Those are chosen by the layout author, not a fixed dm/panel/window vocabulary, so a callback using its own linked variable is clean while a genuinely unbound name is still reported.
  • ${…} string templates are treated as opaque. The content is a template expression, not AmiScript in the surrounding grammar, so it is not reported as a syntax error.

Renaming a Local

Rename and find-references resolve a local to the binding under the cursor, not to every token that shares its name:

1
2
3
int total = 1;
if (ok) { int total = 2; use(total); }   // a different binding, not renamed with the outer one
row.total;                               // a COLUMN that happens to share the name, never renamed

Sibling blocks at the same nesting depth are separate bindings, and a rename started from a member access is declined rather than applied to the wrong thing.

Session Binding

For editor-aware features the language server needs to know which live Web session to resolve callbacks against. The client supplies a session id: the Web session's __SESSIONID, visible via showSessions(). Without a session, the server still works in standalone mode: plain .amisql / .ami files are parsed and validated with a prebuilt method factory, giving completion and diagnostics with no live connection.

Authenticated and Open Endpoints

The LSP is served on 2 paths:

Path Auth Features
/lsp Required (Basic credentials) Full, session-capable language server: live-Center DDL completion, USE-option classification, session editor scope, workspace cross-references
/lsp-open None Partial, forced-standalone server: parser diagnostics, keyword/method/function completion, signature help, hover, formatting, and in-document go-to-definition, computed purely from the buffer. No live data is reachable.

/lsp-open is on by default

LSP_OPEN is part of the shipped ami.rpc.ws.plugin.classes default, so a stock deployment serves an unauthenticated endpoint on the WebSocket port. To keep the port fully authenticated, set the list explicitly without it:

ami.rpc.ws.plugin.classes = DAP,LSP

What it exposes is deliberately narrow: static language tooling computed purely from the caller's own buffers. A sessionId sent by an unauthenticated client is rejected, so no session or Center data is reachable through it. Combined with the loopback default bind address, the practical exposure on a stock install is local-only, but review it before widening ami.rpc.ws.bindAddress.

The DAP Server

What It Debugs

The DAP server attaches your editor's debugger to live AmiScript execution, reaching 2 surfaces through one protocol:

  • Web callbacks (datamodel onProcess, panel callbacks, etc.), debugged in-process against the running Web session.
  • Center targets (timers, triggers, procedures, and methods), debugged through the Center stop-the-world freeze debugger. This works whether the Center is co-located or remote.

You get gutter breakpoints, step over / in / out, the Call Stack and Variables panels, and Watch / hover / inline values.

Target References

A debug attach names its target with one of 2 grammars.

Web callbacks (in-process against a session):

callback:<ari>/<callbackName>
callback:DATAMODEL:<alias>/<callbackName>       e.g.  callback:DATAMODEL:debug/onProcess

Center targets (via the freeze debugger):

1
2
3
4
5
center:<kind>:<name>:<scriptSlot>
    kind = timer | procedure | trigger | method
    e.g.  center:timer:myTimer:script
          center:trigger:myTrig:onInsertingScript
          center:procedure:myProc:script

A Web attach requires a sessionId; a Center attach targets a Center (by id when several are running). Attach resolves the named target before it accepts the session, so a bad id fails immediately instead of leaving you waiting on a breakpoint that can never fire:

  • Web: the callback editor must be available in the session.
  • Center: ami.db.debug.enabled must be on, and the object must exist. Attach checks the name against the Center's live timers, triggers, procedures, and methods and rejects one that is not there, naming the kind and the id. Names are case-sensitive; cross-check with SHOW TIMERS / SHOW TRIGGERS / SHOW PROCEDURES / SHOW METHODS.

    If the Center cannot answer the existence check, because it is already frozen at a breakpoint and the lookup cannot take the read lock, the attach proceeds rather than failing on an unknown. You get the old behavior (a breakpoint that never fires) only in that narrow case.

Protocol Flow

The client sequence is the standard DAP handshake: initialize -> setBreakpoints -> attach -> configurationDone. Breakpoints set before attach are buffered and applied once the target resolves. Stepping maps to the underlying debugger: step-over, step-in, and step-out. Continue runs to the next breakpoint. When a step crosses a call boundary, the adapter follows the freeze into the callee's body so the editor navigates to the code that is actually paused.

Call Stack and Expressions

Each stack frame carries its own source, resolved from that frame's target id, so clicking a caller frame opens the caller's file, not the file you are paused in. Frames from the same target share one source object, which is what lets the editor keep a single tab per target.

evaluate distinguishes what asked for the value:

Context On failure
watch, repl Returns the error, so a bad Watch expression or REPL line tells you why it failed
hover (and unspecified) Stays silent: a hover over an identifier that is not a valid expression in scope shows nothing rather than an error badge

One Controller at a Time

A Center DAP attach takes an exclusive controller claim on the Center debugger. A second DAP attach is refused with a message naming the current holder, rather than 2 editors silently interleaving steps on one frozen actor. The claim is released when the session detaches.

The claim binds IDE clients, not operators

The center_debug command surface deliberately stays available while a DAP session holds the claim. It is the escape hatch you need when the IDE client is wedged or gone. So an operator can still step or resume a freeze that an attached editor is driving, and the two will disagree about where execution is. Coordinate, or use detach first.

A client that dies without releasing, after a crash or on a half-open socket, would otherwise hold the claim until the Center restarts. Clear it from the admin console or MCP:

center_debug("detach")

detach drops the claim only; it does not resume a frozen actor. See Center Debugger.

Security Posture

The WebSocket endpoints default to loopback and are governed by ami.rpc.ws.bindAddress. The /lsp and /dap endpoints are authenticated; /lsp-open is unauthenticated and exposes only static language tooling over the caller's own buffers, with no live data.

2 things to check before widening ami.rpc.ws.bindAddress beyond loopback:

  • /lsp-open ships enabled (see the warning above). Remove LSP_OPEN from ami.rpc.ws.plugin.classes if you do not want a zero-credential lint endpoint reachable off-host.
  • A Center DAP attach can freeze the whole Center at a breakpoint (see Center Debugger). Restrict who can reach /dap, or leave ami.db.debug.enabled off in production.