[BUG] LSP tool passes `rootUri: null` during initialization, breaking TypeScript Language Server on Windows

Resolved 💬 2 comments Opened Mar 6, 2026 by LoveMig6334 Closed Mar 6, 2026

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

  1. Install typescript-language-server globally (via mise .exe shims on Windows)
  2. Open a TypeScript/Next.js project in Claude Code
  3. Ask Claude Code to perform any LSP operation (e.g., "find all references to the Event type, using LSP")
  4. 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

  1. rootUri should be derived automatically: Claude Code should walk up the directory tree from the filePath parameter to find package.json or tsconfig.json and use that as rootUri.
  2. Workspace files should be indexed: After initialized, Claude Code should either send textDocument/didOpen for workspace files, or rely on the server's own tsconfig.json-based file discovery (which requires a valid rootUri).

Workaround

I created a stdio proxy that wraps typescript-language-server's cli.mjs entry point. The proxy:

  1. Intercepts the initialize request and injects rootUri from process.cwd()
  2. After initialized, sends textDocument/didOpen for all .ts/.tsx files in the workspace
  3. 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 | ENOENTuv_spawn can't run .cmd |
| 2 — Volta | bash script shims | ENOENTuv_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 |

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗