Skip to main content
Glama

godot_analyze_script

Read-onlyIdempotent

Analyze GDScript files to detect 10 common pitfalls including API misuse, tight coupling, and signal issues for improved code quality.

Instructions

Analyse GDScript files for all 10 battle-tested pitfalls: Godot 3→4 API misuse, giant scripts, := on Variant, tight coupling, signal re-entrancy, autoload misuse, missing signal disconnect, _init() timing, Python-isms, and static func on autoloads.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
pathYesPath to .gd file (e.g., res://scripts/player.gd)

Implementation Reference

  • The async handler function that executes the godot_analyze_script tool logic - validates the path, reads the script file, calls analyseScript, and returns formatted warnings or success message
    async (args) => {
      if (!ctx.projectDir) {
        return { content: [{ type: "text", text: formatError(projectNotFound()) }] };
      }
    
      const safeResult = resolveSafePath(ctx.projectDir, args.path);
      if ("error" in safeResult) {
        return { content: [{ type: "text", text: safeResult.error }] };
      }
    
      if (!existsSync(safeResult.path)) {
        return {
          content: [
            {
              type: "text",
              text: formatError({
                message: `Script not found: ${args.path}`,
                suggestion: "Check the path and try again.",
              }),
            },
          ],
        };
      }
    
      const content = readFileSync(safeResult.path, "utf-8");
      const warnings = analyseScript(content, safeResult.path, ctx.projectDir);
    
      if (warnings.length === 0) {
        return {
          content: [
            { type: "text", text: `No pitfalls detected in ${args.path}. Script looks clean.` },
          ],
        };
      }
    
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(
              { file: args.path, warningCount: warnings.length, warnings },
              null,
              2
            ),
          },
        ],
      };
    }
  • Zod schema definition for the godot_analyze_script tool input - defines 'path' parameter as a string with description
    {
      path: z
        .string()
        .describe("Path to .gd file (e.g., res://scripts/player.gd)"),
    },
    { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
  • Registers the godot_analyze_script tool with the MCP server via server.tool(), including name, description, schema, hints, and handler
    export function registerScriptAnalysis(server: McpServer, ctx: ServerContext): void {
      server.tool(
        "godot_analyze_script",
        "Analyse GDScript files for all 10 battle-tested pitfalls: Godot 3→4 API misuse, giant scripts, := on Variant, tight coupling, signal re-entrancy, autoload misuse, missing signal disconnect, _init() timing, Python-isms, and static func on autoloads.",
        {
          path: z
            .string()
            .describe("Path to .gd file (e.g., res://scripts/player.gd)"),
        },
        { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
        async (args) => {
          if (!ctx.projectDir) {
            return { content: [{ type: "text", text: formatError(projectNotFound()) }] };
          }
    
          const safeResult = resolveSafePath(ctx.projectDir, args.path);
          if ("error" in safeResult) {
            return { content: [{ type: "text", text: safeResult.error }] };
          }
    
          if (!existsSync(safeResult.path)) {
            return {
              content: [
                {
                  type: "text",
                  text: formatError({
                    message: `Script not found: ${args.path}`,
                    suggestion: "Check the path and try again.",
                  }),
                },
              ],
            };
          }
    
          const content = readFileSync(safeResult.path, "utf-8");
          const warnings = analyseScript(content, safeResult.path, ctx.projectDir);
    
          if (warnings.length === 0) {
            return {
              content: [
                { type: "text", text: `No pitfalls detected in ${args.path}. Script looks clean.` },
              ],
            };
          }
    
          return {
            content: [
              {
                type: "text",
                text: JSON.stringify(
                  { file: args.path, warningCount: warnings.length, warnings },
                  null,
                  2
                ),
              },
            ],
          };
        }
      );
    }
  • The analyseScript helper function containing all 10 pitfall detection patterns for GDScript analysis (Godot 3→4 API, giant scripts, variant type inference, tight coupling, signal re-entrancy, autoload misuse, missing signal disconnect, _init timing, Python-isms)
    function analyseScript(
      content: string,
      filePath: string,
      projectDir: string
    ): ScriptWarning[] {
      const warnings: ScriptWarning[] = [];
      const lines = content.split("\n");
    
      // Pitfall 1: Godot 3→4 API detection
      for (let i = 0; i < lines.length; i++) {
        const line = lines[i];
        // Skip comments
        if (line.trim().startsWith("#")) continue;
    
        for (const pattern of GODOT3_PATTERNS) {
          if (pattern.pattern.test(line)) {
            warnings.push({
              pitfall: "godot3-api",
              severity: "warning",
              line: i + 1,
              message: pattern.message,
              suggestion: pattern.suggestion,
            });
          }
        }
      }
    
      // Pitfall 2: Giant script (>300 lines)
      if (lines.length > 300) {
        warnings.push({
          pitfall: "giant-script",
          severity: "warning",
          line: null,
          message: `Large script (${lines.length} lines).`,
          suggestion:
            "Consider splitting into smaller focused scripts or using composition. Target under 300 lines per script.",
        });
      }
    
      // Pitfall 3: := on Variant return type
      for (let i = 0; i < lines.length; i++) {
        const line = lines[i];
        if (line.trim().startsWith("#")) continue;
    
        // Detect := with Dictionary.get(), Array methods returning Variant, ternary
        if (line.match(/:=\s*.*\.get\s*\(/) || line.match(/:=\s*.*if\s+.*\s+else\s+/)) {
          warnings.push({
            pitfall: "variant-type-inference",
            severity: "warning",
            line: i + 1,
            message: ":= type inference on Variant return.",
            suggestion:
              "Use explicit type annotation or = instead of := to avoid parse errors. " +
              "Example: var value: Variant = dict.get(\"key\", null)",
          });
        }
      }
    
      // Pitfall 4: Tight coupling (excessive get_node/$ references)
      let distantNodeRefs = 0;
      for (let i = 0; i < lines.length; i++) {
        const line = lines[i];
        if (line.trim().startsWith("#")) continue;
    
        // Count get_node or $ with paths that go up (..) or are deeply nested
        const getNodeMatch = line.match(/get_node\s*\(\s*["']([^"']+)["']\s*\)/);
        const dollarMatch = line.match(/\$([A-Za-z0-9_/]+)/);
    
        const path = getNodeMatch?.[1] ?? dollarMatch?.[1];
        if (path && (path.includes("..") || path.split("/").length > 2)) {
          distantNodeRefs++;
        }
      }
      if (distantNodeRefs > 5) {
        warnings.push({
          pitfall: "tight-coupling",
          severity: "warning",
          line: null,
          message: `Tight coupling: ${distantNodeRefs} references to distant nodes.`,
          suggestion:
            "Consider using signals, groups, or dependency injection instead of direct node path references.",
        });
      }
    
      // Pitfall 5: Signal re-entrancy
      // Detect: property assignment, then emit_signal/emit on same indentation level
      for (let i = 0; i < lines.length; i++) {
        const line = lines[i];
        if (line.trim().startsWith("#")) continue;
    
        // Look for emit between state changes
        if (line.match(/\.\s*emit\s*\(/) || line.match(/emit_signal\s*\(/)) {
          // Check if there are state-modifying lines before AND after at same indent
          const indent = line.match(/^(\s*)/)?.[1]?.length ?? 0;
          let stateChangeBefore = false;
          let stateChangeAfter = false;
    
          // Look backwards for state change
          for (let j = i - 1; j >= Math.max(0, i - 10); j--) {
            const prev = lines[j];
            const prevIndent = prev.match(/^(\s*)/)?.[1]?.length ?? 0;
            if (prevIndent < indent && prev.trim().match(/^func\s/)) break;
            if (prevIndent === indent && prev.match(/\s*\w+\s*=(?!=)/)) {
              stateChangeBefore = true;
              break;
            }
          }
    
          // Look forwards for state change
          for (let j = i + 1; j < Math.min(lines.length, i + 10); j++) {
            const next = lines[j];
            const nextIndent = next.match(/^(\s*)/)?.[1]?.length ?? 0;
            if (nextIndent < indent && next.trim() !== "") break;
            if (nextIndent === indent && next.match(/\s*\w+\s*=(?!=)/)) {
              stateChangeAfter = true;
              break;
            }
          }
    
          if (stateChangeBefore && stateChangeAfter) {
            warnings.push({
              pitfall: "signal-re-entrancy",
              severity: "warning",
              line: i + 1,
              message: "Signal re-entrancy risk: signal emitted between state changes.",
              suggestion:
                "Connected handlers will execute synchronously before subsequent lines run. " +
                "Consider emitting signals at the end of state changes, or use call_deferred() for the emission.",
            });
          }
        }
      }
    
      // Pitfall 6: Autoload misuse - static func on autoloads
      // Check if this script is registered as an autoload
      const config = parseProjectGodot(projectDir);
      if (config) {
        const isAutoload = config.autoloads.some(
          (a) => filePath.endsWith(a.path.replace("res://", ""))
        );
        if (isAutoload) {
          for (let i = 0; i < lines.length; i++) {
            if (lines[i].match(/^static\s+func\s/)) {
              warnings.push({
                pitfall: "static-func-autoload",
                severity: "warning",
                line: i + 1,
                message: "static func on autoload script.",
                suggestion:
                  "Godot 4 warns STATIC_CALLED_ON_INSTANCE when calling static methods on autoload instances. " +
                  "Use regular func instead, or move static utilities to a non-autoload class.",
              });
            }
          }
        }
    
        // Check total autoload count
        if (config.autoloads.length > 5) {
          warnings.push({
            pitfall: "too-many-autoloads",
            severity: "warning",
            line: null,
            message: `${config.autoloads.length} autoloads detected.`,
            suggestion:
              "Autoloads are global singletons — only use for genuinely global systems (GameState, AudioManager, SaveSystem). " +
              "Consider using regular classes for utilities.",
          });
        }
      }
    
      // Pitfall 7: Missing signal disconnect
      const connectCalls: Array<{ line: number; signal: string }> = [];
      let hasExitTree = false;
      const disconnectSignals = new Set<string>();
    
      for (let i = 0; i < lines.length; i++) {
        const line = lines[i];
        if (line.trim().startsWith("#")) continue;
    
        // Detect .connect() calls
        const connectMatch = line.match(/(\w+)\.connect\s*\(/);
        if (connectMatch) {
          connectCalls.push({ line: i + 1, signal: connectMatch[1] });
        }
    
        // Detect _exit_tree function
        if (line.match(/^func\s+_exit_tree\s*\(/)) {
          hasExitTree = true;
        }
    
        // Detect .disconnect() calls
        const disconnectMatch = line.match(/(\w+)\.disconnect\s*\(/);
        if (disconnectMatch) {
          disconnectSignals.add(disconnectMatch[1]);
        }
      }
    
      if (connectCalls.length > 0 && !hasExitTree) {
        warnings.push({
          pitfall: "missing-signal-disconnect",
          severity: "warning",
          line: connectCalls[0].line,
          message: "Signal connected without disconnect in _exit_tree().",
          suggestion:
            "This can cause errors when the listening node is freed but the signal source persists. " +
            "Add _exit_tree() and disconnect signals there.",
        });
      }
    
      // Pitfall 8: _init() timing - node tree access in _init
      let inInit = false;
      for (let i = 0; i < lines.length; i++) {
        const line = lines[i];
    
        if (line.match(/^func\s+_init\s*\(/)) {
          inInit = true;
          continue;
        }
        if (inInit && line.match(/^func\s+/) && !line.match(/^func\s+_init/)) {
          inInit = false;
          continue;
        }
    
        if (inInit) {
          if (
            line.match(/get_node\s*\(/) ||
            line.match(/\$[A-Za-z]/) ||
            line.match(/get_parent\s*\(/) ||
            line.match(/get_tree\s*\(/)
          ) {
            warnings.push({
              pitfall: "init-timing",
              severity: "warning",
              line: i + 1,
              message: "Node tree access in _init().",
              suggestion:
                "The node is not in the scene tree during _init(). " +
                "Use _ready() for node access, or _enter_tree() if you need the tree before children are ready.",
            });
          }
        }
      }
    
      // Pitfall 9: Python-isms
      for (let i = 0; i < lines.length; i++) {
        const line = lines[i];
        if (line.trim().startsWith("#")) continue;
    
        // List comprehension syntax
        if (line.match(/\[.+\s+for\s+\w+\s+in\s+/)) {
          warnings.push({
            pitfall: "python-ism",
            severity: "warning",
            line: i + 1,
            message: "Python-style list comprehension detected.",
            suggestion: "GDScript doesn't support list comprehensions. Use Array.map() or a for loop.",
          });
        }
    
        // Python imports
        if (line.match(/^import\s+\w+/) || line.match(/^from\s+\w+\s+import/)) {
          warnings.push({
            pitfall: "python-ism",
            severity: "warning",
            line: i + 1,
            message: "Python-style import detected.",
            suggestion: "GDScript uses preload() or load() for imports, not Python's import syntax.",
          });
        }
    
        // Python builtins
        if (line.match(/\blen\s*\(/) && !line.match(/\.\s*len\s*\(/)) {
          warnings.push({
            pitfall: "python-ism",
            severity: "warning",
            line: i + 1,
            message: "Python-style len() detected.",
            suggestion: "GDScript uses .size() for arrays/strings, not len().",
          });
        }
      }
    
      return warnings;
    }
  • Calls registerScriptAnalysis(server, ctx) to register the godot_analyze_script tool with the MCP server during initialization
    registerScriptAnalysis(server, ctx);
Behavior4/5

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

Annotations already declare readOnlyHint=true, openWorldHint=false, and idempotentHint=true, covering safety and idempotency. The description adds valuable context by specifying the 10 battle-tested pitfalls it analyzes, which goes beyond annotations to explain what the tool actually does behaviorally. No contradiction with annotations.

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 a single, dense sentence that efficiently lists all 10 pitfalls without unnecessary words. It's front-loaded with the core purpose and every element (the pitfalls) earns its place by specifying the tool's scope. No wasted verbiage.

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 complexity (analyzing 10 specific pitfalls) and the presence of annotations but no output schema, the description does well by detailing the pitfalls. However, it doesn't explain the return format or what the analysis output looks like, which is a gap since there's no output schema. It's mostly complete but could benefit from output details.

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%, with the single parameter 'path' clearly documented in the schema. The description doesn't add any parameter-specific information beyond what the schema provides, but it implies the path should point to a .gd file, which aligns with the schema's example. Baseline 3 is appropriate given high schema coverage.

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 explicitly states the verb 'Analyse' and the resource 'GDScript files', providing a specific action and target. It distinguishes from siblings by detailing the 10 specific pitfalls it checks for, unlike the more general 'godot_analyze_scene' or 'godot_get_diagnostics'.

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 clearly indicates this tool is for analyzing GDScript files for specific pitfalls, implying usage when code quality or migration issues are suspected. However, it doesn't explicitly state when NOT to use it or name alternatives among siblings, though the specificity of pitfalls suggests it's for script analysis rather than scene or project-level checks.

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