[BUG] LSP tool passes `rootUri: null` during initialization, breaking TypeScript Language Server on Windows
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
Bug Description
Claude Code's LSP tool sends rootUri: null in the LSP initialize request when spawning typescript-language-server. Without a workspace root, the server cannot locate the project's tsconfig.json or node_modules/typescript, causing it to crash immediately:
Request initialize failed with message: Could not find a valid TypeScript installation.
Please ensure that the "typescript" dependency is installed in the workspace
or that a valid `tsserver.path` is specified. Exiting.
What Should Happen?
Request initialize failed with message: Could not find a valid TypeScript installation.
Please ensure that the "typescript" dependency is installed in the workspace
or that a valid tsserver.path is specified. Exiting.
Error Messages/Logs
● LSP(operation: "findReferences", symbol: "Event", in: "lib\types.ts")
⎿ Error performing findReferences: Request initialize failed with message: Could not find a valid TypeScript installation. Please ensure that the "typescript"
dependency is installed in the workspace or that a valid `tsserver.path` is specified. Exiting.
● LSP(operation: "hover", symbol: "Event", in: "lib\types.ts")
⎿ Error performing hover: Failed to sync file open C:\Users\thatt\Documents\Coding Project\Web Projects\cdsc\lib\types.ts: Cannot send notification to LSP server
'plugin:typescript-lsp:typescript': server is error
● LSP(operation: "documentSymbol", file: "lib\types.ts")
⎿ Error performing documentSymbol: Failed to sync file open C:\Users\thatt\Documents\Coding Project\Web Projects\cdsc\lib\types.ts: Cannot send notification to
LSP server 'plugin:typescript-lsp:typescript': server is error
● LSP(operation: "workspaceSymbol", file: "lib\types.ts")
⎿ Error performing workspaceSymbol: Failed to sync file open C:\Users\thatt\Documents\Coding Project\Web Projects\cdsc\lib\types.ts: Cannot send notification to
LSP server 'plugin:typescript-lsp:typescript': server is error
● LSP(operation: "goToDefinition", symbol: "Event", in: "lib\types.ts")
⎿ Error performing goToDefinition: Failed to sync file open C:\Users\thatt\Documents\Coding Project\Web Projects\cdsc\lib\types.ts: Cannot send notification to
LSP server 'plugin:typescript-lsp:typescript': server is error
● LSP(operation: "goToImplementation", symbol: "Event", in: "lib\types.ts")
⎿ Error performing goToImplementation: Failed to sync file open C:\Users\thatt\Documents\Coding Project\Web Projects\cdsc\lib\types.ts: Cannot send notification
to LSP server 'plugin:typescript-lsp:typescript': server is error
● LSP(operation: "prepareCallHierarchy", file: "lib\types.ts")
⎿ Error performing prepareCallHierarchy: Failed to sync file open C:\Users\thatt\Documents\Coding Project\Web Projects\cdsc\lib\types.ts: Cannot send
notification to LSP server 'plugin:typescript-lsp:typescript': server is error
Steps to Reproduce
Steps to Reproduce
- Install
typescript-language-serverglobally (via mise.exeshims on Windows) - Open a TypeScript/Next.js project in Claude Code
- Ask Claude Code to perform any LSP operation (e.g., "find all references to the Event type, using LSP")
- Observe: server crashes with "Could not find a valid TypeScript installation"
Verification
I manually tested the LSP initialize request with both rootUri: null and the correct workspace URI:
| rootUri value | Result |
|---|---|
| null | Server crashes — cannot find TypeScript |
| "file:///C:/Users/.../cdsc" | Server initializes successfully, finds TypeScript 5.9.3 |
Claude Model
Sonnet (default)
Is this a regression?
No, this never worked
Last Working Version
_No response_
Claude Code Version
2.1.70
Platform
Anthropic API
Operating System
Windows
Terminal/Shell
Windows Terminal
Additional Information
Environment
- Claude Code: v2.1.70
- OS: Windows 11
- Shell: PowerShell 7.5.4 / Git Bash (MSYS2)
- Node manager: mise
- Node.js: v24.14.0
- typescript-language-server: v5.1.3
- TypeScript: v5.9.3
- Project: Next.js 15
Even after fixing rootUri (via a workaround proxy), Claude Code only sends textDocument/didOpen for the single queried file. Cross-file operations like findReferences and goToDefinition return empty results because the server has not indexed the rest of the workspace.
After injecting didOpen for all .ts/.tsx files in the workspace, all operations work correctly:
| Operation | Before fix | After fix |
|---|---|---|
| hover | Server crashed | Fully working with type info |
| documentSymbol | Server crashed | Lists all symbols |
| workspaceSymbol | Server crashed | 651 symbols across workspace |
| findReferences | Server crashed | 9 references across 5 files |
| goToDefinition | Server crashed | Fully working |
Windows Platform Note
On Windows, uv_spawn (used by child_process.spawn) cannot execute .cmd batch files or bash script shims — it requires a real .exe binary. Among tested package managers:
| Package manager | Shim type | Compatible with uv_spawn? |
|---|---|---|
| npm global | .cmd batch file | No |
| Volta | bash script shim | No |
| mise | .exe shim | Yes |
This means mise is currently the only viable way to install typescript-language-server globally on Windows for use with Claude Code's LSP tool.
Expected Behavior
rootUrishould be derived automatically: Claude Code should walk up the directory tree from thefilePathparameter to findpackage.jsonortsconfig.jsonand use that asrootUri.- Workspace files should be indexed: After
initialized, Claude Code should either sendtextDocument/didOpenfor workspace files, or rely on the server's owntsconfig.json-based file discovery (which requires a validrootUri).
Workaround
I created a stdio proxy that wraps typescript-language-server's cli.mjs entry point. The proxy:
- Intercepts the
initializerequest and injectsrootUrifromprocess.cwd() - After
initialized, sendstextDocument/didOpenfor all.ts/.tsxfiles in the workspace - Forwards all other messages unchanged
<details>
<summary>Proxy code (click to expand)</summary>
#!/usr/bin/env node
import { spawn } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { readdirSync, readFileSync, statSync } from 'node:fs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const realCli = join(__dirname, 'cli.original.mjs');
const cwd = process.cwd();
const rootUri = 'file:///' + cwd.replace(/\\/g, '/').replace(/ /g, '%20');
const IGNORE = new Set(['node_modules', '.next', '.git', 'dist', 'build', '.turbo', 'out']);
function getAllTsFiles(dir) {
const results = [];
for (const entry of readdirSync(dir)) {
if (IGNORE.has(entry)) continue;
const full = join(dir, entry);
try {
if (statSync(full).isDirectory()) results.push(...getAllTsFiles(full));
else if (/\.(ts|tsx)$/.test(entry)) results.push(full);
} catch { }
}
return results;
}
const server = spawn(process.execPath, [realCli, ...process.argv.slice(2)], {
env: process.env,
windowsHide: true,
stdio: ['pipe', 'pipe', 'inherit'],
});
server.stdout.pipe(process.stdout);
function sendToServer(msg) {
const out = JSON.stringify(msg);
server.stdin.write(`Content-Length: ${Buffer.byteLength(out, 'utf8')}\r\n\r\n${out}`);
}
function preOpenAllFiles() {
for (const file of getAllTsFiles(cwd)) {
try {
const text = readFileSync(file, 'utf8');
const uri = 'file:///' + file.replace(/\\/g, '/').replace(/ /g, '%20');
const languageId = file.endsWith('.tsx') ? 'typescriptreact' : 'typescript';
sendToServer({
jsonrpc: '2.0',
method: 'textDocument/didOpen',
params: { textDocument: { uri, languageId, version: 1, text } },
});
} catch { }
}
}
let buf = Buffer.alloc(0);
process.stdin.on('data', chunk => {
buf = Buffer.concat([buf, chunk]);
while (true) {
const sep = buf.indexOf('\r\n\r\n');
if (sep === -1) break;
const header = buf.slice(0, sep).toString('ascii');
const m = header.match(/Content-Length:\s*(\d+)/i);
if (!m) { buf = buf.slice(sep + 4); continue; }
const contentLength = parseInt(m[1]);
const bodyStart = sep + 4;
if (buf.length < bodyStart + contentLength) break;
const body = buf.slice(bodyStart, bodyStart + contentLength).toString('utf8');
buf = buf.slice(bodyStart + contentLength);
let msg;
try { msg = JSON.parse(body); } catch { continue; }
if (msg.method === 'initialize' && msg.params && !msg.params.rootUri) {
msg.params.rootUri = rootUri;
msg.params.rootPath = cwd;
}
sendToServer(msg);
if (msg.method === 'initialized') preOpenAllFiles();
}
});
process.stdin.on('end', () => server.stdin.end());
server.on('exit', code => process.exit(code ?? 0));
server.on('error', e => { process.stderr.write('proxy error: ' + e.message + '\n'); process.exit(1); });
</details>
Debugging Timeline
| Session | Setup | Result |
|---|---|---|
| 1 — npm global | .cmd shims | ENOENT — uv_spawn can't run .cmd |
| 2 — Volta | bash script shims | ENOENT — uv_spawn can't run bash scripts |
| 3 — mise | .exe shims | Binary spawns, but crashes: rootUri: null |
| 4 — mise + proxy | .exe + stdio proxy | All LSP operations fully working |
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗