Initial commit

This commit is contained in:
scorpion21211-cmd
2026-10-03 23:29:41 -04:00
commit 965f0d7584
10586 changed files with 1030294 additions and 0 deletions
@@ -0,0 +1,66 @@
@tool
extends RefCounted
## bridge between dispatcher and addon handler
const ErrorCodes := preload("res://addons/godot_ai/utils/error_codes.gd")
var _spec: McpCustomToolSpec
var _locator: McpServiceLocator
var _handler_instance = null # lazily loaded from _spec.script_path
func _init(spec: McpCustomToolSpec, locator: McpServiceLocator) -> void:
_spec = spec
_locator = locator
## Invoked by dispatcher._call_handler as .call(params) — SINGLE ARG.
## Internally splits into (clean_params, ctx) for the addon handler.
func invoke(params: Dictionary) -> Dictionary:
## Dock enable/disable gate: a disabled tool is dropped from the
## catalog push, but a client holding a stale list (or batch_execute)
## can still name it — reject at dispatch too.
var registry := McpToolRegistry.get_instance()
if registry != null and not registry.is_tool_enabled(_spec.name):
return ErrorCodes.make(ErrorCodes.CUSTOM_TOOL_DISABLED,
"Custom tool '%s' is disabled in the Godot AI dock" % _spec.name)
## Readiness gate (https://github.com/hi-godot/godot-ai/issues/781#issuecomment-5036376599 #1): block writes during play/import.
## Checked BEFORE the lazy load so a gated tool's handler script (and any
## _init side effects) never runs while the editor is busy.
var _readiness := McpConnection.get_readiness()
if _spec.requires_writable and (_readiness == "importing" or _readiness == "playing"):
return ErrorCodes.make(ErrorCodes.EDITOR_NOT_READY, "Editor is '%s' — write blocked for custom tool '%s'" % [_readiness, _spec.name])
if _handler_instance == null:
var script := load(_spec.script_path) as GDScript
if script == null:
return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "Cannot load %s" % _spec.script_path)
_handler_instance = script.new() # no-arg; per-call context arrives via ctx
if not _handler_instance.has_method(_spec.method):
return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR, "%s has no method '%s' for custom tool '%s'" % [_spec.script_path, _spec.method, _spec.name])
## Extract _request_id (dispatcher injected it) and strip from params
## so the addon sees clean params matching its declared schema (https://github.com/hi-godot/godot-ai/issues/781#issuecomment-5036376599 #2).
var request_id: String= params.get("_request_id", "")
var clean_params := params.duplicate()
clean_params.erase("_request_id")
## Construct ctx with transport metadata + live-object locator.
var ctx := McpCallContext.new()
ctx.request_id = request_id
ctx.session_id = _locator.get_connection().get_session_id()
ctx.spec = _spec
ctx.attach_locator(_locator)
ctx.deadline_msec = Time.get_ticks_msec() + _spec.timeout_ms
var result: Dictionary = _handler_instance.call(_spec.method, clean_params, ctx)
if result.get("_deferred", false):
## Enforce the declared contract: batch_execute and the server's
## timeout budget both trust spec.deferred, so a non-deferred spec
## whose handler defers anyway would report success while its real
## reply arrives uncorrelated later.
if not _spec.deferred:
return ErrorCodes.make(ErrorCodes.INTERNAL_ERROR,
"Custom tool '%s' returned a deferred response but its spec declares deferred=false" % _spec.name)
if _spec.timeout_ms > 0:
## Handlers return the SHARED McpDispatcher.DEFERRED_RESPONSE
## const, which Godot makes read-only — stamping the per-spec
## budget on it directly is a script error that aborts the call.
result = result.duplicate()
result["_deferred_timeout_ms"] = _spec.timeout_ms
return result