Debugging the Language Server
This guide explains how to debug the WSO2 MI XML Language Server while it's running as a subprocess spawned by the client extension.
Why this is needed
The client (xml-language-client) launches the language server (wso2-mi-language-server-ts) as a separate Node.js subprocess over stdio. Because it's a separate process, you can't debug it just by running the client in VS Code's debugger — you need to open a Node inspector port on the server process and attach to it separately.
One-time setup
1. Use a multi-root workspace
The client and server live in separate folders/repos. Open both together using a
.code-workspace file so VS Code can resolve paths and source maps across both
projects.
In the parent folder containing both repos, create wso2-mi.code-workspace:
{
"folders": [
{ "name": "client", "path": "xml-language-client" },
{ "name": "server", "path": "wso2-mi-language-server-ts" }
],
"settings": {}
}
Open this file via File > Open Workspace from File.... You should see
client and server as separate roots in the Explorer sidebar.
2. Enable source maps in the server build
The server is bundled with esbuild (see
wso2-mi-language-server-ts/package.json). By default, esbuild
does not emit source maps unless told to. Make sure the build
script includes --sourcemap:
"build": "esbuild src/server.ts --bundle --platform=node --format=cjs --sourcemap --outfile=dist/server.js \"--banner:js=const _fileUrl=require('url').pathToFileURL(__filename).href;\" --define:import.meta.url=_fileUrl && node -e \"const fs=require('fs'); fs.rmSync('dist/resources',{recursive:true,force:true}); fs.cpSync('resources','dist/resources',{recursive:true})\""
Without --sourcemap, breakpoints will still "hit" but execution will show in the
compiled dist/server.js instead of the original .ts source.
After any change to this script, rebuild:
cd wso2-mi-language-server-ts
npm run bundle
Confirm dist/server.js.map exists alongside dist/server.js.
3. Add a debug variant to ServerOptions (client side)
In xml-language-client/src/extension.ts, the
ServerOptions object needs a debug variant (separate from
run) that opens a Node inspector port:
const serverModule = path.join(
context.extensionPath,
'..',
'wso2-mi-language-server-ts',
'dist',
'server.js'
)
const serverOptions: ServerOptions = {
run: {
command: process.execPath,
args: [serverModule, '--stdio'],
transport: TransportKind.stdio
},
debug: {
command: process.execPath,
args: ['--nolazy', '--inspect=6009', serverModule, '--stdio'],
transport: TransportKind.stdio
}
}
Notes:
--inspect=6009opens an inspector port on 6009 without pausing execution. If you need to catch issues that happen very early (e.g. during server startup, before you'd have time to attach), temporarily use--inspect-brk=6009instead — this pauses the process immediately until a debugger attaches. Remember to switch it back to--inspectafterward, otherwise you'll need to manually click "Continue" past the pause point on every debug run.- The
debugconfig is only used when the client extension itself is launched in debug mode (see below) —runis what's used in normal/production use. - Double-check
serverModule's path matches your actual server folder name. A stale folder name here causes aMODULE_NOT_FOUNDerror with no useful indication of why the server keeps crashing/restarting.
4. Add launch.json
In xml-language-client/.vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch Extension",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder:client}"
],
"outFiles": [
"${workspaceFolder:client}/dist/**/*.js"
]
},
{
"name": "Attach to Server",
"type": "node",
"request": "attach",
"port": 6009,
"restart": true,
"sourceMaps": true,
"outFiles": [
"${workspaceFolder:server}/dist/**/*.js"
],
"skipFiles": [
"<node_internals>/**"
]
}
],
"compounds": [
{
"name": "Launch + Attach Server",
"configurations": [
"Launch Extension",
"Attach to Server"
]
}
]
}
Key details:
${workspaceFolder:client}/${workspaceFolder:server}are required (not plain${workspaceFolder}) because this is a multi-root workspace — VS Code needs to know which root you mean.outFilesfor "Attach to Server" must point at the server's dist folder, with source maps enabled, or breakpoints will bind to the bundled JS instead of your TypeScript source.skipFiles: ["<node_internals>/**"]prevents the debugger from stepping into Node's internal modules (e.g.promises.js) when stepping through code.
Debugging, day to day
- Make sure the server is built with the latest changes:
cd wso2-mi-language-server-ts npm run bundle - Open the workspace file (
wso2-mi.code-workspace) if it isn't already open. - Set breakpoints in wso2-mi-language-server-ts/src/*.ts —
prefer breakpoints inside handlers (e.g.
onInitialize,onCompletion,onHover) over top-level/module-load code, since those run reliably every time the relevant LSP request fires. - In the Debug panel, select "Launch + Attach Server (client)" from the dropdown.
- Press F5. This launches the Extension Development Host and attaches the Node debugger to port 6009 automatically.
- In the Extension Development Host window, open an
.xmlfile (or whatever triggers the language server) and perform the action tied to your breakpoint (hover, autocomplete, etc.). - Execution should pause at your breakpoint, in the original
.tssource.
Troubleshooting
Breakpoint never hits, execution runs straight through:
- Confirm the server was rebuilt after your latest source changes
(
npm run bundle). - Confirm
dist/server.js.mapexists and isn't stale. - Confirm the breakpoint is on a line that actually executes for the action you're triggering — module-load-only code only runs once, at server startup.
Breakpoint hits but shows the compiled dist/server.js instead of the
.ts source:
- Source maps aren't enabled in the build, or
outFilesinlaunch.jsondoesn't match the actual dist folder. See step 2 above.
Debugger pauses in node_modules or Node internal files like
promises.js:
- If you're using
--inspect-brk, this is expected on the very first pause — just press Continue (F5) once to proceed to your actual breakpoint. - If it happens elsewhere, confirm
skipFilesis set in the "Attach to Server" config.
MODULE_NOT_FOUND errors in the Debug Console, server keeps restarting and
then gives up ("crashed 5 times in the last 3 minutes"):
- The path in
serverModule(inextension.ts) doesn't match the actual server folder/file on disk. Verify with:
from inside the client folder, and make surels ../wso2-mi-language-server-ts/dist/server.jsextension.tswas rebuilt (npm run build) after fixing the path — VS Code runs the compileddist/extension.js, not the TypeScript source directly.