4.9 KiB
C# extensions
C# extensions add the same tools as GDScript ones, but C# cannot extend a GDScript base class, so the mechanics differ. Prefer GDScript unless the project is a .NET project — C# needs a build step before the toolkit can see the tool.
Two hard gotchas (also in the main skill)
- File name must match the class name —
MCPToolkitMyTools.csfor classMCPToolkitMyTools. Godot's source generators only emit script metadata when these match; a mismatch silently fails to register. - Class name must start with
MCPToolkit— this prefix is the C# discovery marker (C# cannot extendMCPToolkitExtension, so the loader recognizes an extension by the prefix on a[GlobalClass]).
Template
C# uses RefCounted directly with duck typing. The options builder is obtained
from the registry factory (C# cannot instantiate the GDScript builder class).
using Godot;
using Godot.Collections;
[Tool, GlobalClass]
public partial class MCPToolkitNotesTools : RefCounted
{
/// Registers this extension's tools. Called once at load.
public void Register(GodotObject registry, Node server)
{
var opts = registry.Call("create_extension_options",
"Write a markdown note to a res:// path").AsGodotObject();
opts.Call("guard_project_path", "file_path");
registry.Call("add", "notes.write",
new Callable(this, MethodName.Write), opts);
}
/// Writes a note. Returns a success envelope or an error dict.
public Dictionary Write(Dictionary parameters)
{
// Parameter extraction, validation, and business logic.
// Return the same {"success": bool, ...} envelope as GDScript.
return new Dictionary { { "success", true }, { "data", "written" } };
}
}
Requirements
[Tool]attribute mandatory — without it the .NET object is not instantiated in the editor (method calls return null).[GlobalClass]attribute mandatory — makes the class visible toProjectSettings.get_global_class_list().partial classextendingRefCounted(notMCPToolkitExtension).- Public
Register(GodotObject registry, Node server)method. - Handler methods accept and return
Godot.Collections.Dictionary. - Callables:
new Callable(this, MethodName.Method)orCallable.From<Dictionary, Dictionary>(Method).
Options builder from the registry factory
C# cannot construct the GDScript builder directly, so call the factory and drive
it by name. Every mark_* / with_* verb works the same as GDScript.
var opts = registry.Call("create_extension_options",
"What this tool does").AsGodotObject();
opts.Call("mark_read_only");
opts.Call("mark_idempotent");
registry.Call("add", "namespace.action", callable, opts);
Discovery workflow (C# needs a build)
- Run
dotnet buildfrom the project root (or click Build in the editor). - This produces the assembly AND updates
global_script_class_cache.cfg. - Alt-tab to the editor (triggers a filesystem scan) or call
extensions_refresh.
No editor restart required — the hot-reload watcher detects the class change
after the rebuild. If the extension still doesn't appear: verify the file name
matches the class name, verify [Tool] and [GlobalClass] are present, and
check the class shows up in .godot/global_script_class_cache.cfg.
Cancellation (C#)
The context arrives as a second GodotObject argument and is driven by name.
public Dictionary HandleFetch(Dictionary parameters, GodotObject ctx)
{
ctx.Connect("cancelled", Callable.From(OnCancelled));
// ... work ...
if ((bool)ctx.Call("is_cancelled")) return new Dictionary();
return new Dictionary { { "success", true }, { "data", result } };
}
Do not store the context — it is scoped to a single invocation.
Headless
Same guard as GDScript, via DisplayServer.GetName() == "headless".
Exporting
C# extensions compile into the project's .NET assembly and cannot be stripped
per-class. Guard editor-only work with if (!Engine.IsEditorHint()) return; so
nothing runs in a build.
Doc comments (///)
Write /// XML-doc comments on the class and each registered handler — they are
valuable to anyone reading your distributed source. Note, however, that Godot
does not harvest C# /// into the in-editor Help (F1) page — only GDScript
## doc comments surface there. So /// documents the code for human readers of
the file; it does not produce an in-editor reference entry the way ## does.
Graceful missing-toolkit handling
C# extensions extend RefCounted, so they compile fine even when the toolkit is
absent — Register() is simply never called (a silent no-op). For an
EditorPlugin wrapper, check
EditorInterface.is_plugin_enabled("godot_mcp_toolkit") in _enter_tree() and
push_warning() with install instructions (search "Godot MCP Toolkit" in the
Godot AssetLib). State the dependency prominently in your README either way.