Makes TypeScript LSP servers work with Claude Code on Windows. Supports both the official typescript-lsp plugin (uses typescript-language-server) and the third-party vtsls plugin from claude-code-lsps (uses vtsls).
Related issues:
-
.cmdspawn failure: Claude Code uses Node.jschild_process.spawn()withoutshell: true, which can't execute npm's.cmd/shell script wrappers on Windows (ENOENT/uv_spawnerrors). -
Inconsistent file URIs: Claude Code's LSP client sends different URI formats for the same file:
textDocument/didOpen:file://D:\path\to\file.ts(backslashes, double slash — malformed)textDocument/hoveretc.:file:///D:/path/to/file.ts(forward slashes, triple slash — correct)
The server registers the file under the
didOpenURI but can't find it when queried with the other format, so all type-aware operations (hover,findReferences,documentSymbol) return empty results. Only parsing-based operations (workspaceSymbol) work. -
Large message corruption: A string-based LSP message parser breaks on large messages (like
didOpenfor big files) becauseContent-Lengthcounts bytes but string slicing counts characters.
A lightweight Node.js proxy that sits between Claude Code and the language server:
- Normalizes all
file://URIs to consistentfile:///D:/...format - Uses
Buffer-based message framing (byte-accurate) - A compiled
.exeshim bypasses the.cmdspawn issue - One shim binary works for both servers — it detects which to launch from its own filename
- Node.js on PATH
- Claude Code with a TypeScript LSP plugin installed (official or vtsls)
- PowerShell (for compiling the C# shim)
Install the language server(s) you want to use globally:
# For the official typescript-lsp plugin:
npm i -g typescript-language-server typescript
# For the vtsls plugin:
npm i -g @vtsls/language-server-
Save
lsp-proxy.mjsandlsp-shim.csto%USERPROFILE%\.claude\ -
Compile the exe shim for whichever server(s) you use:
# For vtsls plugin:
Add-Type -OutputType ConsoleApplication `
-OutputAssembly "$env:APPDATA\npm\vtsls.exe" `
-Path "$env:USERPROFILE\.claude\lsp-shim.cs"
# For official typescript-lsp plugin:
Add-Type -OutputType ConsoleApplication `
-OutputAssembly "$env:APPDATA\npm\typescript-language-server.exe" `
-Path "$env:USERPROFILE\.claude\lsp-shim.cs"The same source produces both — the exe detects which server to launch from its own filename.
- Restart Claude Code.
Set LSP_PROXY_LOG=1 environment variable to write all LSP messages to lsp-proxy.log in the current working directory.
Claude Code → vtsls.exe (shim) → node lsp-proxy.mjs node vtsls.js --stdio → vtsls
Claude Code → typescript-language-server.exe (shim) → node lsp-proxy.mjs node cli.mjs --stdio → tsserver
↕ normalize URIs
↕ buffer-based framing
The exe shim exists solely to work around the .cmd spawn issue. The proxy does the actual work. Both shims are compiled from the same lsp-shim.cs — the binary inspects argv[0] to decide which server to launch.