Giving Claude diagnostics and a debugger in Godot

by Fireal Software · ~7 min read

If you ask Claude “are there errors in my project”, and it has nothing but file access, the best it can do is read every script and guess. No type inference. No cross-file symbol resolution. No way to know that you renamed a method in player.gd and forgot to update the caller in enemy.gd.

Godot already has the answer to this. The editor ships a GDScript Language Server on port 6005 and a Debug Adapter on port 6006. Both speak standard protocols every IDE knows. Godot Catalyst plugs Claude into them directly.

Why LSP beats reading files

The Language Server Protocol is what VS Code, Neovim, and Zed all use to get real-time diagnostics, completions, hovers, and go-to-definition. It runs as a separate process (or in Godot’s case, a thread inside the editor) that parses every .gd file as you type, maintains a full symbol table, and answers queries by position.

When Claude needs to know “what errors are in this script”, the LSP already knows the answer. It has the parse tree, the type information, the cross-reference graph. Asking the LSP is O(1) at the cost of a JSON-RPC call. Asking Claude to read the file and reason about syntax errors is O(lines × tokens) and prone to missing subtle issues — GDScript’s type system has edge cases Claude won’t always catch.

Godot Catalyst’s LSP client (src/transport/lsp-client.ts) connects to port 6005 on first use, initializes with a standard initialize request advertising the capabilities we care about (completion, hover, definition, references, symbols, formatting, rename, diagnostics), and stays connected. The first godot_lsp_diagnostics call takes ~200ms for the LSP to open the document and publish diagnostics. Subsequent calls on the same document are ~10ms.

The LSP tools exposed to Claude:

That last one — lsp_rename — is the heavy lift. Renaming a method that’s called from five scripts used to mean find-and-replace with grep. Now Claude calls the LSP and Godot’s own renamer walks the reference graph. Cross-file, type-aware, correct.

Why DAP beats print-debugging

The Debug Adapter Protocol is what VS Code’s debugger uses. It standardizes “launch this program”, “set a breakpoint at line 42 of this file”, “step over”, “give me the variables in the current stack frame” across languages. Godot 4’s DAP server lets you attach an external debugger to a running game.

Without DAP, a Claude debugging session looks like: add print() statements, run the game, read the output, remove the prints, try again. Slow and mutates the code.

With DAP, Claude sets a breakpoint, runs the scene, the game pauses on the hit, Claude pulls the variable values at that point, decides what to do next.

src/transport/dap-client.ts connects to port 6006, sends initialize, then launch with the scene path. The DAP server starts the game in debug mode, reports a stopped event when a breakpoint hits, and Claude can:

A typical session: “There’s a bug where the enemy’s health goes negative. Set a breakpoint on the damage function and tell me what’s in the damage_amount variable when the hit lands.”

Claude runs godot_debug_set_breakpoints, godot_debug_launch, plays through until the breakpoint hits, runs godot_debug_variables to inspect the frame, and reports back. Zero print statements added. Zero git noise.

Content-Length framing

Both LSP and DAP use the same transport envelope:

Content-Length: 234\r\n
\r\n
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{...}}

The header tells the receiver how many bytes to read for the body. This is necessary because the JSON payloads can be large (a workspace symbol query can return hundreds of kilobytes) and TCP doesn’t preserve message boundaries.

The LSP client’s _onData method in src/transport/lsp-client.ts handles the framing. Buffer incoming bytes, look for the \r\n\r\n header separator, parse the Content-Length, wait for that many body bytes, dispatch the message, repeat. The DAP client’s transport is nearly identical — same framing, slightly different JSON envelope ({seq, type, command, arguments} instead of {jsonrpc, id, method, params}).

If the framing is wrong — missing newlines, off-by-one content length — the whole stream desynchronizes and every subsequent message fails. This is why both clients do strict parsing and reject malformed frames rather than trying to recover.

Lazy init is load-bearing

The LSP and DAP sockets cost real resources (a TCP connection, an LSP document store, per-document diagnostic state). Godot Catalyst opens them on first use, not at server startup.

async ensureReady(): Promise<void> {
  if (this.initialized) return;
  if (this.initializing) return this.initializing;
  this.initializing = this._init();
  try { await this.initializing; } catch (e) { this.initializing = null; throw e; }
}

Every LSP/DAP tool call starts with await client.ensureReady(). If the connection is up, it returns immediately. If init is in-flight, it waits for the same promise. If nothing’s happened yet, it connects.

This matters because most Godot sessions don’t use LSP or DAP. Someone editing a tilemap doesn’t need the language server. Paying the init cost on startup would slow down every godot_create_node by several hundred milliseconds. Lazy init keeps the common case fast and makes the specialist tools opt-in.

The connection errors have good messages

If the LSP is off and Claude calls godot_lsp_diagnostics, the connect fails and the client throws:

“Cannot connect to Godot LSP server on port 6005. Ensure Editor → Editor Settings → Network → Language Server is enabled.”

Same pattern for DAP on port 6006. This is intentional. An agent failing silently on a misconfigured user is the worst outcome — it’ll retry, burn tokens, and maybe fall back to reading files and doing a bad job. A loud error that tells the user exactly which setting to flip is faster to fix.

Breakpoint orchestration

Setting a breakpoint on line 42 of player.gd is one tool call. But a real debugging session chains several:

godot_debug_set_breakpoints({"path": "res://scripts/player.gd", "lines": [42]})
godot_debug_launch({"scene_path": "res://scenes/main.tscn"})
// ... game runs, hits the breakpoint ...
godot_debug_stack_trace()
godot_debug_variables({"frame_id": 0})
godot_debug_step_over()
godot_debug_continue()
godot_debug_terminate()

Each call is small. The server keeps state: it tracks the current stopped thread, the last stack frame, whether the session is terminated. Claude can chain them naturally without managing the state itself.

Turn on LSP and DAP in Editor > Editor Settings > Network. Both are off by default. LSP costs nothing to run idle; DAP only activates when you launch a debug session.

Turn Claude into a Godot co-developer

Godot Catalyst is an MCP server with 240+ tools for Godot 4.x. GDScript LSP, DAP debugging, offline parsing, asset pipelines. 7-day free trial, $25 one-time.

Try Godot Catalyst