Skip to main content
Glama

godot_get_diagnostics

Read-onlyIdempotent

Retrieve LSP diagnostics (errors and warnings) from Godot's built-in language server to identify code issues in your project or specific files.

Instructions

Get LSP diagnostics (errors, warnings) from Godot's built-in language server. Requires Godot editor to be running with the project open.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
pathNoFile path to get diagnostics for (e.g., scripts/player.gd). Omit for project-wide.
portNoLSP port (default: 6005)

Implementation Reference

  • The main handler function that executes the godot_get_diagnostics tool logic - connects to Godot's LSP server, initializes the connection, resolves safe paths, and retrieves diagnostics for specified files.
      async (args) => {
        if (!ctx.projectDir) {
          return { content: [{ type: "text", text: formatError(projectNotFound()) }] };
        }
    
        const port = args.port ?? 6005;
    
        if (!lspClient) {
          lspClient = new GodotLspClient();
        }
    
        const connected = await lspClient.connect(port);
        if (!connected) {
          return {
            content: [
              {
                type: "text",
                text: formatError({
                  message: `Godot editor LSP not available on port ${port}.`,
                  suggestion:
                    "Start the Godot editor with your project open to enable LSP diagnostics. " +
                    "The LSP runs automatically when the editor is open. " +
                    "All non-LSP tools (test runner, docs search, script analysis, etc.) continue to work without the editor.",
                }),
              },
            ],
          };
        }
    
        const initOk = await lspClient.initialize(ctx.projectDir);
        if (!initOk) {
          return {
            content: [
              {
                type: "text",
                text: formatError({
                  message: "Failed to initialise LSP handshake with Godot editor.",
                  suggestion: "Try restarting the Godot editor and ensure the project is open.",
                }),
              },
            ],
          };
        }
    
        try {
          if (args.path) {
            const safeResult = resolveSafePath(ctx.projectDir, args.path);
            if ("error" in safeResult) {
              return { content: [{ type: "text", text: safeResult.error }] };
            }
    
            const uri = `file://${safeResult.path}`;
            const diagnostics = await lspClient.getDiagnosticsForFile(safeResult.path, uri);
    
            return {
              content: [{ type: "text", text: JSON.stringify(diagnostics, null, 2) }],
            };
          }
    
          // Project-wide: no efficient way to get all diagnostics without opening every file.
          // Return a helpful message suggesting per-file usage.
          return {
            content: [
              {
                type: "text",
                text: formatError({
                  message: "Project-wide diagnostics require specifying a file path.",
                  suggestion:
                    "Godot's LSP pushes diagnostics per-file. Use the `path` argument to check specific files, " +
                    "e.g., path: \"scripts/player.gd\". For broad analysis, use `godot_analyze_script` instead — " +
                    "it works without the editor running.",
                }),
              },
            ],
          };
        } catch (err) {
          const message = err instanceof Error ? err.message : "Unknown error";
          // Reset client on error so next call reconnects
          lspClient.disconnect();
          lspClient = null;
    
          return {
            content: [
              {
                type: "text",
                text: formatError({
                  message: `LSP request failed: ${message}`,
                  suggestion: "Ensure the Godot editor is running and the project is open.",
                }),
              },
            ],
          };
        }
      }
    );
  • Registration of godot_get_diagnostics tool with the MCP server, including tool name, description, input schema (path and port parameters), hints, and handler function.
    export function registerLspDiagnostics(server: McpServer, ctx: ServerContext): void {
      server.tool(
        "godot_get_diagnostics",
        "Get LSP diagnostics (errors, warnings) from Godot's built-in language server. Requires Godot editor to be running with the project open.",
        {
          path: z
            .string()
            .optional()
            .describe("File path to get diagnostics for (e.g., scripts/player.gd). Omit for project-wide."),
          port: z
            .number()
            .optional()
            .describe("LSP port (default: 6005)"),
        },
        { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
        async (args) => {
          if (!ctx.projectDir) {
            return { content: [{ type: "text", text: formatError(projectNotFound()) }] };
          }
    
          const port = args.port ?? 6005;
    
          if (!lspClient) {
            lspClient = new GodotLspClient();
          }
    
          const connected = await lspClient.connect(port);
          if (!connected) {
            return {
              content: [
                {
                  type: "text",
                  text: formatError({
                    message: `Godot editor LSP not available on port ${port}.`,
                    suggestion:
                      "Start the Godot editor with your project open to enable LSP diagnostics. " +
                      "The LSP runs automatically when the editor is open. " +
                      "All non-LSP tools (test runner, docs search, script analysis, etc.) continue to work without the editor.",
                  }),
                },
              ],
            };
          }
    
          const initOk = await lspClient.initialize(ctx.projectDir);
          if (!initOk) {
            return {
              content: [
                {
                  type: "text",
                  text: formatError({
                    message: "Failed to initialise LSP handshake with Godot editor.",
                    suggestion: "Try restarting the Godot editor and ensure the project is open.",
                  }),
                },
              ],
            };
          }
    
          try {
            if (args.path) {
              const safeResult = resolveSafePath(ctx.projectDir, args.path);
              if ("error" in safeResult) {
                return { content: [{ type: "text", text: safeResult.error }] };
              }
    
              const uri = `file://${safeResult.path}`;
              const diagnostics = await lspClient.getDiagnosticsForFile(safeResult.path, uri);
    
              return {
                content: [{ type: "text", text: JSON.stringify(diagnostics, null, 2) }],
              };
            }
    
            // Project-wide: no efficient way to get all diagnostics without opening every file.
            // Return a helpful message suggesting per-file usage.
            return {
              content: [
                {
                  type: "text",
                  text: formatError({
                    message: "Project-wide diagnostics require specifying a file path.",
                    suggestion:
                      "Godot's LSP pushes diagnostics per-file. Use the `path` argument to check specific files, " +
                      "e.g., path: \"scripts/player.gd\". For broad analysis, use `godot_analyze_script` instead — " +
                      "it works without the editor running.",
                  }),
                },
              ],
            };
          } catch (err) {
            const message = err instanceof Error ? err.message : "Unknown error";
            // Reset client on error so next call reconnects
            lspClient.disconnect();
            lspClient = null;
    
            return {
              content: [
                {
                  type: "text",
                  text: formatError({
                    message: `LSP request failed: ${message}`,
                    suggestion: "Ensure the Godot editor is running and the project is open.",
                  }),
                },
              ],
            };
          }
        }
      );
    }
  • Diagnostic interface defining the output schema for each diagnostic item returned by the tool (severity, message, file, line, column).
    interface Diagnostic {
      severity: "error" | "warning" | "info" | "hint";
      message: string;
      file: string;
      line: number;
      column: number;
    }
  • GodotLspClient class that manages the LSP connection to Godot's built-in language server - handles socket connection, message buffering, request/response handling, and the getDiagnosticsForFile method.
    class GodotLspClient {
      private socket: Socket | null = null;
      private connected = false;
      private initialized = false;
      private messageBuffer = "";
      private nextId = 1;
      private responseHandlers = new Map<number, { resolve: (msg: LspMessage) => void; reject: (err: Error) => void }>();
      private notificationHandlers: Array<(msg: LspMessage) => void> = [];
    
      async connect(port: number): Promise<boolean> {
        if (this.connected && this.socket) return true;
    
        return new Promise((resolve) => {
          const socket = new Socket();
          const timeout = setTimeout(() => {
            socket.destroy();
            resolve(false);
          }, 3000);
    
          socket.connect(port, "127.0.0.1", () => {
            clearTimeout(timeout);
            this.socket = socket;
            this.connected = true;
    
            socket.on("data", (data) => {
              this.messageBuffer += data.toString();
              this.processBuffer();
            });
    
            socket.on("close", () => {
              this.connected = false;
              this.socket = null;
              this.initialized = false;
            });
    
            socket.on("error", () => {
              this.connected = false;
              this.socket = null;
              this.initialized = false;
            });
    
            resolve(true);
          });
    
          socket.on("error", () => {
            clearTimeout(timeout);
            resolve(false);
          });
        });
      }
    
      private processBuffer(): void {
        while (true) {
          const headerEnd = this.messageBuffer.indexOf("\r\n\r\n");
          if (headerEnd === -1) break;
          const header = this.messageBuffer.substring(0, headerEnd);
          const lengthMatch = header.match(/Content-Length:\s*(\d+)/i);
          if (!lengthMatch) break;
          const contentLength = parseInt(lengthMatch[1], 10);
          const contentStart = headerEnd + 4;
          if (this.messageBuffer.length < contentStart + contentLength) break;
          const content = this.messageBuffer.substring(contentStart, contentStart + contentLength);
          this.messageBuffer = this.messageBuffer.substring(contentStart + contentLength);
          try {
            const msg = JSON.parse(content) as LspMessage;
            this.handleMessage(msg);
          } catch {
            // skip malformed
          }
        }
      }
    
      private handleMessage(msg: LspMessage): void {
        if (msg.id !== undefined && this.responseHandlers.has(msg.id)) {
          const handler = this.responseHandlers.get(msg.id)!;
          this.responseHandlers.delete(msg.id);
          handler.resolve(msg);
        } else {
          // Notification (no id, or unsolicited)
          for (const handler of this.notificationHandlers) {
            handler(msg);
          }
        }
      }
    
      private send(method: string, params: unknown, id?: number): void {
        if (!this.socket || !this.connected) return;
        const msg: Record<string, unknown> = { jsonrpc: "2.0", method, params };
        if (id !== undefined) msg.id = id;
        const body = JSON.stringify(msg);
        const header = `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n`;
        this.socket.write(header + body);
      }
    
      private sendRequest(method: string, params: unknown): Promise<LspMessage> {
        return new Promise((resolve, reject) => {
          const id = this.nextId++;
          this.responseHandlers.set(id, { resolve, reject });
          this.send(method, params, id);
          setTimeout(() => {
            if (this.responseHandlers.has(id)) {
              this.responseHandlers.delete(id);
              reject(new Error(`LSP request '${method}' timed out`));
            }
          }, 5000);
        });
      }
    
      private sendNotification(method: string, params: unknown): void {
        this.send(method, params);
      }
    
      async initialize(projectDir: string): Promise<boolean> {
        if (this.initialized) return true;
    
        try {
          await this.sendRequest("initialize", {
            processId: process.pid,
            rootUri: `file://${projectDir}`,
            capabilities: {
              textDocument: {
                publishDiagnostics: { relatedInformation: true },
              },
            },
          });
    
          this.sendNotification("initialized", {});
          this.initialized = true;
          return true;
        } catch {
          return false;
        }
      }
    
      /**
       * Open a file and collect diagnostics pushed by the server.
       * Godot sends textDocument/publishDiagnostics notifications after didOpen.
       */
      async getDiagnosticsForFile(filePath: string, uri: string): Promise<Diagnostic[]> {
        const diagnostics: Diagnostic[] = [];
    
        // Read file content for didOpen
        let text: string;
        try {
          text = readFileSync(filePath, "utf-8");
        } catch {
          return [];
        }
    
        return new Promise((resolve) => {
          const collected: Diagnostic[] = [];
          let resolved = false;
    
          const handler = (msg: LspMessage) => {
            if (msg.method === "textDocument/publishDiagnostics") {
              const params = msg.params as {
                uri: string;
                diagnostics: Array<{
                  severity: number;
                  message: string;
                  range: { start: { line: number; character: number } };
                }>;
              };
    
              if (params.uri === uri) {
                for (const d of params.diagnostics) {
                  collected.push({
                    severity: severityToString(d.severity),
                    message: d.message,
                    file: uri.replace("file://", ""),
                    line: d.range.start.line + 1,
                    column: d.range.start.character + 1,
                  });
                }
    
                // Resolve after receiving diagnostics
                if (!resolved) {
                  resolved = true;
                  cleanup();
                  resolve(collected);
                }
              }
            }
          };
    
          const cleanup = () => {
            const idx = this.notificationHandlers.indexOf(handler);
            if (idx !== -1) this.notificationHandlers.splice(idx, 1);
          };
    
          this.notificationHandlers.push(handler);
    
          // Send didOpen
          this.sendNotification("textDocument/didOpen", {
            textDocument: {
              uri,
              languageId: "gdscript",
              version: 1,
              text,
            },
          });
    
          // If no diagnostics arrive within 3 seconds, return empty (file is clean)
          setTimeout(() => {
            if (!resolved) {
              resolved = true;
              cleanup();
              resolve(collected);
            }
          }, 3000);
        });
      }
    
      disconnect(): void {
        if (this.socket) {
          this.socket.destroy();
          this.socket = null;
          this.connected = false;
          this.initialized = false;
        }
      }
    }
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true, openWorldHint=false, and idempotentHint=true, indicating a safe, deterministic read operation. The description adds useful context beyond annotations by specifying the prerequisite (Godot editor running with project open), which is not covered by annotations. However, it does not detail behavioral aspects like rate limits, error handling, or response format.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is front-loaded with the core purpose in the first sentence and adds a prerequisite in the second, with zero wasted words. It is appropriately sized for a tool with two parameters and clear annotations, making it efficient and easy to parse.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's moderate complexity (2 parameters, no output schema), the description is mostly complete. It covers the purpose, prerequisite, and scope, but lacks details on output format (e.g., structure of diagnostics) and error cases. With annotations providing safety and determinism hints, and schema covering parameters, the description does well but could improve by explaining return values.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema fully documents both parameters (path and port). The description does not add meaning beyond the schema, such as explaining diagnostic types or project-wide implications, but it implies the tool's scope (project-wide if path omitted). Baseline 3 is appropriate as the schema handles parameter documentation adequately.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the specific action ('Get LSP diagnostics') and resource ('from Godot's built-in language server'), distinguishing it from siblings like godot_analyze_scene or godot_analyze_script by focusing on language server diagnostics rather than scene/script analysis. It specifies the content (errors, warnings) and the source (Godot's built-in language server).

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides clear context for when to use the tool: when Godot editor is running with the project open. It implies usage for retrieving diagnostics but does not explicitly state when not to use it or name alternatives among siblings, such as godot_analyze_script for script-specific analysis without LSP.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/gregario/godot-forge'

If you have feedback or need assistance with the MCP directory API, please join our Discord server