Initial commit
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user