Last active
January 8, 2026 15:38
-
-
Save icanhasjonas/4c04328b8c886277399ad23f41549571 to your computer and use it in GitHub Desktop.
Bun adapter for csharp-ls to work with Claude Code LSP (fixes workDoneProgress/registerCapability issues)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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<number | string, string>(); | |
| // 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); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment