WSO2 MI Language Server
/ Debugging the Language Server
Development

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=6009 opens 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=6009 instead — this pauses the process immediately until a debugger attaches. Remember to switch it back to --inspect afterward, otherwise you'll need to manually click "Continue" past the pause point on every debug run.
  • The debug config is only used when the client extension itself is launched in debug mode (see below) — run is what's used in normal/production use.
  • Double-check serverModule's path matches your actual server folder name. A stale folder name here causes a MODULE_NOT_FOUND error 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.
  • outFiles for "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

  1. Make sure the server is built with the latest changes:
    cd wso2-mi-language-server-ts
    npm run bundle
  2. Open the workspace file (wso2-mi.code-workspace) if it isn't already open.
  3. 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.
  4. In the Debug panel, select "Launch + Attach Server (client)" from the dropdown.
  5. Press F5. This launches the Extension Development Host and attaches the Node debugger to port 6009 automatically.
  6. In the Extension Development Host window, open an .xml file (or whatever triggers the language server) and perform the action tied to your breakpoint (hover, autocomplete, etc.).
  7. Execution should pause at your breakpoint, in the original .ts source.

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.map exists 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 outFiles in launch.json doesn'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 skipFiles is 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 (in extension.ts) doesn't match the actual server folder/file on disk. Verify with:
    ls ../wso2-mi-language-server-ts/dist/server.js
    from inside the client folder, and make sure extension.ts was rebuilt (npm run build) after fixing the path — VS Code runs the compiled dist/extension.js, not the TypeScript source directly.