247 lines
10 KiB
GDScript3
247 lines
10 KiB
GDScript3
@tool
|
|
class_name MCPToolkitCommandOptions
|
|
extends RefCounted
|
|
## Fluent builder for the options a command declares when registered.
|
|
##
|
|
## Every chainable method returns [code]self[/code], so a registration reads as a
|
|
## single chain whose terminal [method to_dict] produces the metadata dictionary
|
|
## [method MCPToolkitCommandRegistry.add] consumes. Each setter records one facet
|
|
## of the command's contract: its hints ([method mark_read_only],
|
|
## [method mark_destructive], [method mark_idempotent]), its dispatch behaviour
|
|
## ([method mark_exclusive_execution], [method mark_scene_independent],
|
|
## [method with_timeout_ms]), its grouping ([method with_group]), its version range
|
|
## ([method with_min_godot_version] / [method with_max_godot_version]), and its
|
|
## path guards ([method guard_project_path] / [method guard_user_path]).[br]
|
|
## [br]
|
|
## For built-in tools, only [method mark_read_only] is load-bearing
|
|
## (routing/serialization); the client-visible readOnly/destructive/idempotent hints
|
|
## are server-authoritative (server [code]src/catalogue.ts[/code]).
|
|
## [method mark_destructive] / [method mark_idempotent] apply only to EXTENSION
|
|
## tools, whose annotations the bridge reads via
|
|
## [method MCPToolkitCommandRegistry.get_command_metadata]. Extension tools should
|
|
## use [MCPToolkitExtensionOptions], a subclass that requires a description at
|
|
## construction time (it errors on an empty one but still constructs).[br]
|
|
## [br]
|
|
## Example — register a read-only physics tool:
|
|
## [codeblock]
|
|
## registry.add("physics_list_bodies", _on_list_bodies,
|
|
## MCPToolkitCommandOptions.new() \
|
|
## .mark_read_only() \
|
|
## .with_timeout_ms(60000) \
|
|
## .with_group("physics_tools", "Physics inspection", ["physics", "force"]))
|
|
## [/codeblock]
|
|
|
|
var _description: String = ""
|
|
var _input_schema: Dictionary = {}
|
|
var _is_read_only: bool = false
|
|
var _is_destructive: bool = false
|
|
var _is_idempotent: bool = false
|
|
var _is_cancellable: bool = false
|
|
var _timeout_ms: int = 0 # 0 = use registry default (30000)
|
|
var _group_name: String = ""
|
|
var _group_description: String = ""
|
|
var _group_keywords: Array = []
|
|
var _is_scene_independent: bool = false # inverted: true means NOT required
|
|
var _exclusive_execution: bool = false
|
|
var _min_godot_version: String = ""
|
|
var _max_godot_version: String = ""
|
|
var _success_hint: String = ""
|
|
var _path_guards: Dictionary = {} # param_name -> "project" | "user"
|
|
|
|
|
|
## Sets the human-readable [param description] the bridge advertises for an
|
|
## extension tool (built-in descriptions are server-authoritative).
|
|
## Returns [code]self[/code] for chaining. Prefer [MCPToolkitExtensionOptions],
|
|
## which takes the description in its constructor.
|
|
func with_description(description: String) -> MCPToolkitCommandOptions:
|
|
_description = description
|
|
return self
|
|
|
|
|
|
## Sets the JSON-Schema [param schema] describing the tool's parameters, used by
|
|
## the bridge to validate and advertise the tool's inputs.
|
|
## Returns [code]self[/code] for chaining.
|
|
func with_input_schema(schema: Dictionary) -> MCPToolkitCommandOptions:
|
|
_input_schema = schema
|
|
return self
|
|
|
|
|
|
## Sets the per-command watchdog deadline to [param timeout] milliseconds.
|
|
## A non-positive value (the default) means "use the registry default" (30 s);
|
|
## the registry floors a positive value at 1 s and caps it at 300 s.
|
|
## Returns [code]self[/code] for chaining.
|
|
func with_timeout_ms(timeout: int) -> MCPToolkitCommandOptions:
|
|
_timeout_ms = timeout
|
|
return self
|
|
|
|
|
|
## Assigns the command to the on-demand group [param name], so it loads only when
|
|
## that group is requested rather than at startup. [param description] and
|
|
## [param keywords] help the bridge present and surface the group. Pass an empty
|
|
## [param name] to leave the command ungrouped (eager).
|
|
## Returns [code]self[/code] for chaining.
|
|
func with_group(name: String, description: String = "",
|
|
keywords: Array = []) -> MCPToolkitCommandOptions:
|
|
_group_name = name
|
|
_group_description = description
|
|
_group_keywords = keywords
|
|
return self
|
|
|
|
|
|
## Marks the command read-only: it does not mutate editor or project state.
|
|
## This is load-bearing for every command — the dispatcher uses it to decide
|
|
## whether the call must be serialized against mutations (read-only calls bypass
|
|
## the mutation queue) — and for extension tools it also becomes the client-visible
|
|
## [code]readOnlyHint[/code]. Mutually exclusive with [method mark_destructive].
|
|
## Returns [code]self[/code] for chaining.
|
|
func mark_read_only() -> MCPToolkitCommandOptions:
|
|
_is_read_only = true
|
|
return self
|
|
|
|
|
|
## Marks the command destructive (it may delete or overwrite data), surfacing the
|
|
## client-visible [code]destructiveHint[/code] for an extension tool. Applies to
|
|
## EXTENSION tools only — built-in hints are server-authoritative. The registry
|
|
## treats this as mutually exclusive with [method mark_read_only].
|
|
## Returns [code]self[/code] for chaining.
|
|
func mark_destructive() -> MCPToolkitCommandOptions:
|
|
_is_destructive = true
|
|
return self
|
|
|
|
|
|
## Marks the command idempotent (repeating it with the same arguments has the same
|
|
## effect as calling it once), surfacing the client-visible
|
|
## [code]idempotentHint[/code] for an extension tool. Applies to EXTENSION tools
|
|
## only — built-in hints are server-authoritative.
|
|
## Returns [code]self[/code] for chaining.
|
|
func mark_idempotent() -> MCPToolkitCommandOptions:
|
|
_is_idempotent = true
|
|
return self
|
|
|
|
|
|
## Marks the command cancellable, so the dispatcher passes a
|
|
## [MCPToolkitToolContext] the handler can poll to abort cooperatively.
|
|
## Returns [code]self[/code] for chaining.
|
|
func mark_cancellable() -> MCPToolkitCommandOptions:
|
|
_is_cancellable = true
|
|
return self
|
|
|
|
|
|
## Marks the command as not requiring an open edited scene, so the dispatcher
|
|
## does not reject it when no scene is active. By default a command IS treated as
|
|
## requiring the active scene; call this for scene-independent tools (e.g. ones
|
|
## that only read project files).
|
|
## Returns [code]self[/code] for chaining.
|
|
func mark_scene_independent() -> MCPToolkitCommandOptions:
|
|
_is_scene_independent = true
|
|
return self
|
|
|
|
|
|
## Marks the command for exclusive execution, so the dispatcher serializes it
|
|
## against all other mutations even when it is also read-only (use for commands
|
|
## whose work must not overlap any other dispatch).
|
|
## Returns [code]self[/code] for chaining.
|
|
func mark_exclusive_execution() -> MCPToolkitCommandOptions:
|
|
_exclusive_execution = true
|
|
return self
|
|
|
|
|
|
## Restricts the command to a minimum engine version: when the running engine is
|
|
## older than [param version], the registry skips registration entirely so the
|
|
## command never appears. [param version] is [code]"major.minor"[/code] or
|
|
## [code]"major.minor.patch"[/code] (e.g. [code]"4.5"[/code]); an invalid format
|
|
## warns and is otherwise ignored. See [method with_max_godot_version] for the
|
|
## upper bound. Returns [code]self[/code] for chaining.
|
|
func with_min_godot_version(version: String) -> MCPToolkitCommandOptions:
|
|
if not _is_valid_version(version):
|
|
push_warning("[MCPToolkit] Invalid min_godot_version format: '%s' (expected 'major.minor', e.g. '4.5')" % version)
|
|
_min_godot_version = version
|
|
return self
|
|
|
|
|
|
## Restricts the command to a maximum engine version: when the running engine is
|
|
## newer than [param version], the registry skips registration. [param version]
|
|
## uses the same format as [method with_min_godot_version]; an invalid format
|
|
## warns and is ignored. Returns [code]self[/code] for chaining.
|
|
func with_max_godot_version(version: String) -> MCPToolkitCommandOptions:
|
|
if not _is_valid_version(version):
|
|
push_warning("[MCPToolkit] Invalid max_godot_version format: '%s' (expected 'major.minor', e.g. '4.6')" % version)
|
|
_max_godot_version = version
|
|
return self
|
|
|
|
|
|
## Sets the [param hint] auto-attached to a successful response when the handler
|
|
## did not set one itself, giving the caller follow-up guidance.
|
|
## Returns [code]self[/code] for chaining.
|
|
func with_success_hint(hint: String) -> MCPToolkitCommandOptions:
|
|
_success_hint = hint
|
|
return self
|
|
|
|
|
|
## Declares that the parameter named [param param] carries a [code]res://[/code]
|
|
## project path the dispatcher must validate (rejecting traversal/escape with
|
|
## [code]PATH_DENIED[/code]) BEFORE the handler runs. Use this for any LLM-supplied
|
|
## path an extension command reads or writes; built-in tools self-guard, so this is
|
|
## the declarative equivalent for extension commands. An absent or empty value at
|
|
## call time is left for the handler (it is not a rejection). See
|
|
## [method guard_user_path] for the [code]user://[/code] variant.
|
|
## Returns [code]self[/code] for chaining.
|
|
func guard_project_path(param: String) -> MCPToolkitCommandOptions:
|
|
_path_guards[param] = "project"
|
|
return self
|
|
|
|
|
|
## Like [method guard_project_path], but declares that the parameter named
|
|
## [param param] carries a [code]user://[/code] path. The dispatcher rejects
|
|
## traversal, non-[code]user://[/code] prefixes, and the plugin's own internal
|
|
## paths with a path-denied error before the handler runs.
|
|
## Returns [code]self[/code] for chaining.
|
|
func guard_user_path(param: String) -> MCPToolkitCommandOptions:
|
|
_path_guards[param] = "user"
|
|
return self
|
|
|
|
|
|
static func _is_valid_version(v: String) -> bool:
|
|
var parts := v.split(".")
|
|
if parts.size() < 2:
|
|
return false
|
|
return parts[0].is_valid_int() and parts[1].is_valid_int()
|
|
|
|
|
|
## Builds the options dictionary the registry stores for the command, collapsing
|
|
## every facet set on this builder into one metadata map. Keys with no meaningful
|
|
## value are omitted; [code]is_active_scene_required[/code] is the inverse of
|
|
## [method mark_scene_independent]. [method MCPToolkitCommandRegistry.add] calls
|
|
## this — you rarely call it directly. Returns the metadata dictionary.
|
|
func to_dict() -> Dictionary:
|
|
var d := {
|
|
"description": _description,
|
|
"is_read_only": _is_read_only,
|
|
"is_destructive": _is_destructive,
|
|
"is_idempotent": _is_idempotent,
|
|
"is_cancellable": _is_cancellable,
|
|
"is_active_scene_required": not _is_scene_independent,
|
|
}
|
|
if _exclusive_execution:
|
|
d["exclusive_execution"] = true
|
|
if not _input_schema.is_empty():
|
|
d["input_schema"] = _input_schema
|
|
if _timeout_ms > 0:
|
|
d["timeout_ms"] = _timeout_ms
|
|
if _group_name != "":
|
|
var group := {"name": _group_name}
|
|
if _group_description != "":
|
|
group["description"] = _group_description
|
|
if not _group_keywords.is_empty():
|
|
group["keywords"] = _group_keywords
|
|
d["group"] = group
|
|
if _min_godot_version != "":
|
|
d["min_godot_version"] = _min_godot_version
|
|
if _max_godot_version != "":
|
|
d["max_godot_version"] = _max_godot_version
|
|
if _success_hint != "":
|
|
d["success_hint"] = _success_hint
|
|
if not _path_guards.is_empty():
|
|
d["path_guards"] = _path_guards
|
|
return d
|