#!/usr/bin/env bun /** * LSP Adapter for csharp-ls * * Proxies LSP messages between Claude Code and csharp-ls, * handling unsupported methods that crash csharp-ls. * * Intercepts (responds with null to csharp-ls, doesn't forward to Claude): * - window/workDoneProgress/create -> progress token creation * - window/workDoneProgress/cancel -> progress cancellation * - client/registerCapability -> dynamic capability registration * * Passes through (Claude Code handles these): * - workspace/configuration -> Claude Code returns config * - Everything else */ import { spawn } from "bun"; import { dirname, join } from "path"; const DEBUG = process.env.LSP_ADAPTER_DEBUG === "1"; // Find the original csharp-ls binary (sibling to this script) const SCRIPT_DIR = dirname(Bun.main); const CSHARP_LS_ORIGINAL = join(SCRIPT_DIR, "csharp-ls-original"); function log(...args: any[]) { if (DEBUG) { console.error("[ADAPTER]", ...args); } } // LSP Message parser state class LspParser { private buffer = Buffer.alloc(0); private contentLength = -1; parse(chunk: Buffer): Array<{ header: string; body: any }> { this.buffer = Buffer.concat([this.buffer, chunk]); const messages: Array<{ header: string; body: any }> = []; while (true) { if (this.contentLength === -1) { // Looking for Content-Length header const headerEnd = this.buffer.indexOf("\r\n\r\n"); if (headerEnd === -1) break; const header = this.buffer.subarray(0, headerEnd).toString(); const match = header.match(/Content-Length:\s*(\d+)/i); if (!match) { log("Invalid header:", header); break; } this.contentLength = parseInt(match[1], 10); this.buffer = this.buffer.subarray(headerEnd + 4); } if (this.buffer.length < this.contentLength) break; const bodyStr = this.buffer.subarray(0, this.contentLength).toString(); this.buffer = this.buffer.subarray(this.contentLength); this.contentLength = -1; try { const body = JSON.parse(bodyStr); messages.push({ header: `Content-Length: ${bodyStr.length}\r\n\r\n`, body }); } catch (e) { log("Failed to parse JSON:", bodyStr); } } return messages; } } function encodeMessage(body: any): Buffer { const content = JSON.stringify(body); const header = `Content-Length: ${Buffer.byteLength(content)}\r\n\r\n`; return Buffer.from(header + content); } // Methods we intercept and handle ourselves (respond with null, don't forward to Claude) const INTERCEPTED_METHODS = new Set([ "window/workDoneProgress/create", "window/workDoneProgress/cancel", "client/registerCapability", ]); async function main() { log("Starting csharp-ls adapter..."); // Spawn the real csharp-ls (original binary renamed, sibling to this script) const csharpLs = spawn([CSHARP_LS_ORIGINAL], { stdin: "pipe", stdout: "pipe", stderr: "inherit", }); log("Spawned csharp-ls, pid:", csharpLs.pid); const clientParser = new LspParser(); // Claude Code -> Adapter const serverParser = new LspParser(); // csharp-ls -> Adapter // Pending requests from server that we need to respond to const pendingServerRequests = new Map(); // Forward stdin (from Claude Code) to csharp-ls (async () => { const reader = Bun.stdin.stream().getReader(); while (true) { const { done, value } = await reader.read(); if (done) { log("Client stdin closed"); csharpLs.stdin.end(); break; } const messages = clientParser.parse(Buffer.from(value)); for (const msg of messages) { log("CLIENT ->", msg.body.method || msg.body.id); // Pass all client messages through unchanged csharpLs.stdin.write(encodeMessage(msg.body)); } if (messages.length === 0 && value.length > 0) { // Partial message, buffer is handling it } } })(); // Forward stdout (from csharp-ls) to Claude Code, intercepting specific methods (async () => { const reader = csharpLs.stdout.getReader(); while (true) { const { done, value } = await reader.read(); if (done) { log("Server stdout closed"); break; } const messages = serverParser.parse(Buffer.from(value)); for (const msg of messages) { const { body } = msg; // Check if this is a request we should intercept if (body.method && INTERCEPTED_METHODS.has(body.method)) { log("INTERCEPT <-", body.method, "id:", body.id); // Send success response back to csharp-ls if (body.id !== undefined) { const response = { jsonrpc: "2.0", id: body.id, result: null }; csharpLs.stdin.write(encodeMessage(response)); log("INTERCEPT ->", "response for", body.method); } // Don't forward to client continue; } log("SERVER ->", body.method || `response:${body.id}`); // Forward to Claude Code process.stdout.write(encodeMessage(body)); } } })(); // Handle process termination process.on("SIGINT", () => { log("SIGINT received, killing csharp-ls"); csharpLs.kill(); process.exit(0); }); process.on("SIGTERM", () => { log("SIGTERM received, killing csharp-ls"); csharpLs.kill(); process.exit(0); }); // Wait for csharp-ls to exit const exitCode = await csharpLs.exited; log("csharp-ls exited with code:", exitCode); process.exit(exitCode); } main().catch((err) => { console.error("[ADAPTER] Fatal error:", err); process.exit(1); });