breakpoint-mcp 1.2.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/addon/breakpoint_mcp/README.md +8 -1
- package/addon/breakpoint_mcp/bridge_server.gd +13 -0
- package/addon/breakpoint_mcp/operations.gd +1 -1
- package/addon/breakpoint_mcp/plugin.cfg +1 -1
- package/addon/breakpoint_mcp/plugin.gd +13 -0
- package/addon/breakpoint_mcp/status_dock.gd +312 -0
- package/dist/index.js +1 -1
- package/dist/schemas.js +16 -0
- package/dist/tools/lsp.js +113 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
The MCP **host** for [Breakpoint MCP](https://github.com/jlivingston-Cipher/godot-breakpoint-mcp) —
|
|
4
4
|
a [Model Context Protocol](https://modelcontextprotocol.io) server that exposes the
|
|
5
5
|
Godot game engine to AI coding assistants across four planes: headless CLI, the live
|
|
6
|
-
editor, Godot's own LSP + DAP, and a runtime bridge inside the running game. **
|
|
6
|
+
editor, Godot's own LSP + DAP, and a runtime bridge inside the running game. **244 tools
|
|
7
7
|
+ 5 MCP resources**, built against the stable `@modelcontextprotocol/sdk` 1.x API.
|
|
8
8
|
Developed and tested with **Claude**; because MCP is an open protocol, other clients can
|
|
9
9
|
connect too (currently untested — reports welcome).
|
|
@@ -9,9 +9,16 @@ Loopback TCP/JSON bridge that exposes the live Godot editor (and, via the runtim
|
|
|
9
9
|
|
|
10
10
|
Requires **Godot 4.2+** (uses the `EditorInterface` singleton; 4.4+ recommended).
|
|
11
11
|
|
|
12
|
+
## Status dock
|
|
13
|
+
On enable, a **Breakpoint MCP** panel is added to the editor dock. It reports the health of
|
|
14
|
+
the editor / runtime / GDScript-LSP / DAP bridges, shows the configured ports and project
|
|
15
|
+
path, and offers a one-click **Copy MCP-client config**. Connection / status / config only —
|
|
16
|
+
not a chat UI; the AI assistant runs in your MCP client.
|
|
17
|
+
|
|
12
18
|
## Files
|
|
13
|
-
- `plugin.gd` — `EditorPlugin` entry point; starts/stops the server.
|
|
19
|
+
- `plugin.gd` — `EditorPlugin` entry point; starts/stops the server and adds the status dock.
|
|
14
20
|
- `bridge_server.gd` — `TCPServer` polled from `_process`; newline-delimited JSON framing.
|
|
21
|
+
- `status_dock.gd` — in-editor status/config dock (bridge health, ports, copy client config).
|
|
15
22
|
- `operations.gd` — request handlers; every mutation is wrapped in `EditorUndoRedoManager`.
|
|
16
23
|
- `variant_json.gd` — Variant ⇄ JSON codec (tagged rich types).
|
|
17
24
|
|
|
@@ -48,6 +48,19 @@ func shutdown() -> void:
|
|
|
48
48
|
_server = null
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
## Read-only snapshot for the in-editor status dock. Pure — never mutates state.
|
|
52
|
+
## `listening` reflects the live TCPServer; `clients` is the current peer count.
|
|
53
|
+
## The dock lives inside this same process, so this is authoritative for the
|
|
54
|
+
## editor-bridge plane (no TCP self-probe needed).
|
|
55
|
+
func get_status() -> Dictionary:
|
|
56
|
+
return {
|
|
57
|
+
"listening": _server != null and _server.is_listening(),
|
|
58
|
+
"host": "127.0.0.1",
|
|
59
|
+
"port": _port,
|
|
60
|
+
"clients": _clients.size(),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
51
64
|
func _process(_delta: float) -> void:
|
|
52
65
|
if _server == null:
|
|
53
66
|
return
|
|
@@ -7,7 +7,7 @@ extends RefCounted
|
|
|
7
7
|
## wrapped in the EditorUndoRedoManager so a human can Ctrl-Z anything the assistant did.
|
|
8
8
|
|
|
9
9
|
const Codec := preload("res://addons/breakpoint_mcp/variant_json.gd")
|
|
10
|
-
const ADDON_VERSION := "1.
|
|
10
|
+
const ADDON_VERSION := "1.4.0"
|
|
11
11
|
|
|
12
12
|
var _plugin: EditorPlugin
|
|
13
13
|
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
name="Breakpoint MCP"
|
|
4
4
|
description="Exposes the live Godot editor and the running game to an MCP host so an AI assistant can drive them: scene/node/resource CRUD with full undo/redo, project settings, ClassDB, editor + in-game screenshots, and an in-game runtime bridge (live SceneTree, property get/set, method calls, signals, input injection, Performance monitors). Pairs with the breakpoint-mcp MCP host."
|
|
5
5
|
author="James Livingston"
|
|
6
|
-
version="1.
|
|
6
|
+
version="1.4.0"
|
|
7
7
|
script="plugin.gd"
|
|
@@ -12,10 +12,12 @@ extends EditorPlugin
|
|
|
12
12
|
## subscribed MCP host can emit notifications/resources/updated.
|
|
13
13
|
|
|
14
14
|
const BridgeServer := preload("res://addons/breakpoint_mcp/bridge_server.gd")
|
|
15
|
+
const StatusDock := preload("res://addons/breakpoint_mcp/status_dock.gd")
|
|
15
16
|
const RUNTIME_AUTOLOAD := "BreakpointRuntimeBridge"
|
|
16
17
|
const RUNTIME_SCRIPT := "res://addons/breakpoint_mcp/runtime_bridge.gd"
|
|
17
18
|
|
|
18
19
|
var _server: Node = null
|
|
20
|
+
var _dock: Control = null
|
|
19
21
|
var _selection: EditorSelection = null
|
|
20
22
|
|
|
21
23
|
|
|
@@ -24,6 +26,13 @@ func _enter_tree() -> void:
|
|
|
24
26
|
_server.name = "BreakpointBridgeServer"
|
|
25
27
|
_server.setup(self)
|
|
26
28
|
add_child(_server)
|
|
29
|
+
# Phase-4 status/config dock: a thin panel that reports bridge health across
|
|
30
|
+
# the editor/runtime/LSP/DAP planes and offers a one-click MCP-client config.
|
|
31
|
+
# Connection/status/config only — not a chat UI. Reads the server in-process.
|
|
32
|
+
var dock := StatusDock.new()
|
|
33
|
+
dock.set_server(_server)
|
|
34
|
+
add_control_to_dock(DOCK_SLOT_RIGHT_UL, dock)
|
|
35
|
+
_dock = dock
|
|
27
36
|
# Inject the runtime bridge into every run of the project so the runtime_*
|
|
28
37
|
# tools work as soon as the game starts. Removed cleanly on disable.
|
|
29
38
|
add_autoload_singleton(RUNTIME_AUTOLOAD, RUNTIME_SCRIPT)
|
|
@@ -38,6 +47,10 @@ func _enter_tree() -> void:
|
|
|
38
47
|
|
|
39
48
|
|
|
40
49
|
func _exit_tree() -> void:
|
|
50
|
+
if _dock:
|
|
51
|
+
remove_control_from_docks(_dock)
|
|
52
|
+
_dock.queue_free()
|
|
53
|
+
_dock = null
|
|
41
54
|
if _selection and _selection.selection_changed.is_connected(_on_selection_changed):
|
|
42
55
|
_selection.selection_changed.disconnect(_on_selection_changed)
|
|
43
56
|
_selection = null
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
@tool
|
|
2
|
+
extends VBoxContainer
|
|
3
|
+
## Breakpoint MCP — in-editor status/config dock (Phase-4 adoption item).
|
|
4
|
+
##
|
|
5
|
+
## The GUI twin of `breakpoint-mcp doctor` + `init`: it surfaces the four bridge
|
|
6
|
+
## planes' health, the configured ports, the project path, and a one-click
|
|
7
|
+
## "Copy MCP-client config" yielding the exact snippet `init` prints. Scope is
|
|
8
|
+
## connection / status / config ONLY — it is NOT a chat UI. The AI assistant
|
|
9
|
+
## still lives in the user's MCP client; this dock only wires it up and reports.
|
|
10
|
+
##
|
|
11
|
+
## The editor-bridge plane is read directly from the in-process bridge server
|
|
12
|
+
## (server.get_status()); the runtime / LSP / DAP planes are checked with short,
|
|
13
|
+
## non-blocking StreamPeerTCP probes ticked from _process, so the editor main
|
|
14
|
+
## thread never stalls. LSP/DAP ports are read from EditorSettings, so the dock
|
|
15
|
+
## reflects the user's actual configuration, not just the defaults.
|
|
16
|
+
|
|
17
|
+
const DEFAULT_BRIDGE_PORT := 9080
|
|
18
|
+
const DEFAULT_RUNTIME_PORT := 9081
|
|
19
|
+
const DEFAULT_LSP_PORT := 6005
|
|
20
|
+
const DEFAULT_DAP_PORT := 6006
|
|
21
|
+
const SERVER_NAME := "godot"
|
|
22
|
+
const REFRESH_INTERVAL_SEC := 2.0
|
|
23
|
+
const PROBE_DEADLINE_MSEC := 1500
|
|
24
|
+
|
|
25
|
+
const COLOR_OK := Color(0.38, 0.85, 0.45)
|
|
26
|
+
const COLOR_FAIL := Color(0.92, 0.47, 0.40)
|
|
27
|
+
const COLOR_PENDING := Color(0.62, 0.62, 0.62)
|
|
28
|
+
|
|
29
|
+
const PLANES := [
|
|
30
|
+
{"key": "editor", "name": "editor-bridge"},
|
|
31
|
+
{"key": "runtime", "name": "runtime-bridge"},
|
|
32
|
+
{"key": "lsp", "name": "gdscript-lsp"},
|
|
33
|
+
{"key": "dap", "name": "gdscript-dap"},
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# --- pure helpers: editor-free, socket-free, unit-tested --------------------
|
|
38
|
+
|
|
39
|
+
## The stdio server entry an MCP client config needs, matching the host's
|
|
40
|
+
## clients.ts serverEntry() default (GODOT_BIN omitted when it is the default).
|
|
41
|
+
static func server_entry(project_path: String) -> Dictionary:
|
|
42
|
+
return {
|
|
43
|
+
"command": "npx",
|
|
44
|
+
"args": ["-y", "breakpoint-mcp"],
|
|
45
|
+
"env": {"GODOT_PROJECT": project_path},
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
## Copy-pasteable single-server snippet (wrapper key "mcpServers"), pretty JSON
|
|
49
|
+
## with 2-space indent and insertion order preserved (sort_keys=false) so it is
|
|
50
|
+
## the same shape `breakpoint-mcp init` prints — dock and CLI never drift.
|
|
51
|
+
static func client_snippet(project_path: String) -> String:
|
|
52
|
+
var obj := {"mcpServers": {SERVER_NAME: server_entry(project_path)}}
|
|
53
|
+
return JSON.stringify(obj, " ", false)
|
|
54
|
+
|
|
55
|
+
## Status glyph, mirroring doctor's check / cross / dash vocabulary.
|
|
56
|
+
static func plane_glyph(status: String) -> String:
|
|
57
|
+
match status:
|
|
58
|
+
"ok":
|
|
59
|
+
return "✓"
|
|
60
|
+
"fail":
|
|
61
|
+
return "✗"
|
|
62
|
+
_:
|
|
63
|
+
return "–"
|
|
64
|
+
|
|
65
|
+
## One plane row's text: "<glyph> <name> <detail>".
|
|
66
|
+
static func plane_line(pname: String, status: String, detail: String) -> String:
|
|
67
|
+
return "%s %s %s" % [plane_glyph(status), pname, detail]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# --- state -----------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
var _server: Node = null # bridge_server.gd instance, injected by plugin.gd
|
|
73
|
+
var _rows := {} # plane key -> Label
|
|
74
|
+
var _probes := {} # plane key -> {peer, deadline, port}
|
|
75
|
+
var _config_label: Label = null
|
|
76
|
+
var _copy_feedback: Label = null
|
|
77
|
+
var _timer: Timer = null
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
## plugin.gd injects the live bridge server so the editor-bridge plane is read
|
|
81
|
+
## in-process (authoritative) instead of self-probing the loopback port.
|
|
82
|
+
func set_server(server: Node) -> void:
|
|
83
|
+
_server = server
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
func _ready() -> void:
|
|
87
|
+
_build_ui()
|
|
88
|
+
_timer = Timer.new()
|
|
89
|
+
_timer.wait_time = REFRESH_INTERVAL_SEC
|
|
90
|
+
_timer.autostart = true
|
|
91
|
+
_timer.timeout.connect(_refresh)
|
|
92
|
+
add_child(_timer)
|
|
93
|
+
set_process(true)
|
|
94
|
+
_refresh()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
func _exit_tree() -> void:
|
|
98
|
+
for key in _probes:
|
|
99
|
+
var pr: Dictionary = _probes[key]
|
|
100
|
+
if pr["peer"] != null:
|
|
101
|
+
pr["peer"].disconnect_from_host()
|
|
102
|
+
_probes.clear()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# --- UI --------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
func _build_ui() -> void:
|
|
108
|
+
name = "Breakpoint"
|
|
109
|
+
add_theme_constant_override("separation", 6)
|
|
110
|
+
|
|
111
|
+
var header := Label.new()
|
|
112
|
+
header.text = "Breakpoint MCP"
|
|
113
|
+
header.add_theme_font_size_override("font_size", 15)
|
|
114
|
+
add_child(header)
|
|
115
|
+
|
|
116
|
+
var sub := Label.new()
|
|
117
|
+
sub.text = "bridge status · ports · client setup"
|
|
118
|
+
sub.modulate = Color(1, 1, 1, 0.6)
|
|
119
|
+
add_child(sub)
|
|
120
|
+
|
|
121
|
+
add_child(HSeparator.new())
|
|
122
|
+
|
|
123
|
+
var bridges_hdr := Label.new()
|
|
124
|
+
bridges_hdr.text = "Bridges"
|
|
125
|
+
bridges_hdr.modulate = Color(1, 1, 1, 0.75)
|
|
126
|
+
add_child(bridges_hdr)
|
|
127
|
+
|
|
128
|
+
for p in PLANES:
|
|
129
|
+
var row := Label.new()
|
|
130
|
+
row.add_theme_font_size_override("font_size", 13)
|
|
131
|
+
_rows[p["key"]] = row
|
|
132
|
+
add_child(row)
|
|
133
|
+
_set_plane(p["key"], "pending", "…")
|
|
134
|
+
add_child(HSeparator.new())
|
|
135
|
+
|
|
136
|
+
var cfg_hdr := Label.new()
|
|
137
|
+
cfg_hdr.text = "Config"
|
|
138
|
+
cfg_hdr.modulate = Color(1, 1, 1, 0.75)
|
|
139
|
+
add_child(cfg_hdr)
|
|
140
|
+
|
|
141
|
+
_config_label = Label.new()
|
|
142
|
+
_config_label.add_theme_font_size_override("font_size", 12)
|
|
143
|
+
_config_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
144
|
+
add_child(_config_label)
|
|
145
|
+
|
|
146
|
+
add_child(HSeparator.new())
|
|
147
|
+
|
|
148
|
+
var copy_btn := Button.new()
|
|
149
|
+
copy_btn.text = "Copy MCP-client config"
|
|
150
|
+
copy_btn.tooltip_text = "Copy the mcpServers snippet for this project to the clipboard."
|
|
151
|
+
copy_btn.pressed.connect(_on_copy_pressed)
|
|
152
|
+
add_child(copy_btn)
|
|
153
|
+
|
|
154
|
+
var refresh_btn := Button.new()
|
|
155
|
+
refresh_btn.text = "Refresh"
|
|
156
|
+
refresh_btn.pressed.connect(_refresh)
|
|
157
|
+
add_child(refresh_btn)
|
|
158
|
+
|
|
159
|
+
_copy_feedback = Label.new()
|
|
160
|
+
_copy_feedback.add_theme_font_size_override("font_size", 12)
|
|
161
|
+
_copy_feedback.modulate = Color(1, 1, 1, 0.7)
|
|
162
|
+
_copy_feedback.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
163
|
+
add_child(_copy_feedback)
|
|
164
|
+
|
|
165
|
+
var foot := Label.new()
|
|
166
|
+
foot.text = "Status/config only — the assistant runs in your MCP client, not here."
|
|
167
|
+
foot.modulate = Color(1, 1, 1, 0.5)
|
|
168
|
+
foot.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
169
|
+
add_child(foot)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
func _display_name(key: String) -> String:
|
|
173
|
+
for p in PLANES:
|
|
174
|
+
if p["key"] == key:
|
|
175
|
+
return String(p["name"])
|
|
176
|
+
return key
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
func _set_plane(key: String, status: String, detail: String) -> void:
|
|
180
|
+
var row: Label = _rows.get(key)
|
|
181
|
+
if row == null:
|
|
182
|
+
return
|
|
183
|
+
row.text = plane_line(_display_name(key), status, detail)
|
|
184
|
+
var col := COLOR_PENDING
|
|
185
|
+
if status == "ok":
|
|
186
|
+
col = COLOR_OK
|
|
187
|
+
elif status == "fail":
|
|
188
|
+
col = COLOR_FAIL
|
|
189
|
+
row.add_theme_color_override("font_color", col)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# --- refresh ---------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
func _refresh() -> void:
|
|
195
|
+
_update_editor_plane()
|
|
196
|
+
_start_probe("runtime", _runtime_port())
|
|
197
|
+
_start_probe("lsp", _lsp_port())
|
|
198
|
+
_start_probe("dap", _dap_port())
|
|
199
|
+
_update_config_line()
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
func _update_editor_plane() -> void:
|
|
203
|
+
if _server != null and _server.has_method("get_status"):
|
|
204
|
+
var s: Dictionary = _server.get_status()
|
|
205
|
+
if bool(s.get("listening", false)):
|
|
206
|
+
var n := int(s.get("clients", 0))
|
|
207
|
+
var host := String(s.get("host", "127.0.0.1"))
|
|
208
|
+
var suffix := "client" if n == 1 else "clients"
|
|
209
|
+
_set_plane("editor", "ok", "%s:%d · %d %s" % [host, int(s.get("port", DEFAULT_BRIDGE_PORT)), n, suffix])
|
|
210
|
+
else:
|
|
211
|
+
_set_plane("editor", "fail", "not listening")
|
|
212
|
+
else:
|
|
213
|
+
_set_plane("editor", "fail", "server unavailable")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
func _update_config_line() -> void:
|
|
217
|
+
if _config_label == null:
|
|
218
|
+
return
|
|
219
|
+
_config_label.text = "project %s\neditor %d · runtime %d · lsp %d · dap %d" % [
|
|
220
|
+
_project_path(), _bridge_port(), _runtime_port(), _lsp_port(), _dap_port(),
|
|
221
|
+
]
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
func _on_copy_pressed() -> void:
|
|
225
|
+
DisplayServer.clipboard_set(client_snippet(_project_path()))
|
|
226
|
+
if _copy_feedback != null:
|
|
227
|
+
_copy_feedback.text = "Copied the mcpServers snippet to the clipboard."
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# --- ports -----------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
func _project_path() -> String:
|
|
233
|
+
return ProjectSettings.globalize_path("res://").trim_suffix("/")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
func _bridge_port() -> int:
|
|
237
|
+
if _server != null and _server.has_method("get_status"):
|
|
238
|
+
var s: Dictionary = _server.get_status()
|
|
239
|
+
return int(s.get("port", DEFAULT_BRIDGE_PORT))
|
|
240
|
+
return _env_port("BREAKPOINT_BRIDGE_PORT", DEFAULT_BRIDGE_PORT)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
func _runtime_port() -> int:
|
|
244
|
+
return _env_port("BREAKPOINT_RUNTIME_PORT", DEFAULT_RUNTIME_PORT)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
func _env_port(var_name: String, fallback: int) -> int:
|
|
248
|
+
var env := OS.get_environment(var_name)
|
|
249
|
+
if env != "" and env.is_valid_int():
|
|
250
|
+
return int(env)
|
|
251
|
+
return fallback
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
func _editor_setting_int(key: String, fallback: int) -> int:
|
|
255
|
+
var es := EditorInterface.get_editor_settings()
|
|
256
|
+
if es != null and es.has_setting(key):
|
|
257
|
+
var v: Variant = es.get_setting(key)
|
|
258
|
+
if typeof(v) == TYPE_INT or typeof(v) == TYPE_FLOAT:
|
|
259
|
+
return int(v)
|
|
260
|
+
return fallback
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
func _lsp_port() -> int:
|
|
264
|
+
return _editor_setting_int("network/language_server/remote_port", DEFAULT_LSP_PORT)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
func _dap_port() -> int:
|
|
268
|
+
return _editor_setting_int("network/debug_adapter/remote_port", DEFAULT_DAP_PORT)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# --- non-blocking TCP probes -----------------------------------------------
|
|
272
|
+
# The editor-bridge plane is read in-process (see _update_editor_plane); only
|
|
273
|
+
# runtime / LSP / DAP are probed. connect_to_host is non-blocking; the socket is
|
|
274
|
+
# polled from _process across frames, so a slow/closed port never stalls the UI.
|
|
275
|
+
|
|
276
|
+
func _start_probe(key: String, port: int) -> void:
|
|
277
|
+
if _probes.has(key):
|
|
278
|
+
var old: Dictionary = _probes[key]
|
|
279
|
+
if old["peer"] != null:
|
|
280
|
+
old["peer"].disconnect_from_host()
|
|
281
|
+
_probes.erase(key)
|
|
282
|
+
var peer := StreamPeerTCP.new()
|
|
283
|
+
var err := peer.connect_to_host("127.0.0.1", port)
|
|
284
|
+
if err != OK:
|
|
285
|
+
_set_plane(key, "fail", "127.0.0.1:%d unreachable" % port)
|
|
286
|
+
return
|
|
287
|
+
_set_plane(key, "pending", "127.0.0.1:%d checking…" % port)
|
|
288
|
+
_probes[key] = {"peer": peer, "deadline": Time.get_ticks_msec() + PROBE_DEADLINE_MSEC, "port": port}
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
func _process(_delta: float) -> void:
|
|
292
|
+
if _probes.is_empty():
|
|
293
|
+
return
|
|
294
|
+
var done: Array = []
|
|
295
|
+
for key in _probes:
|
|
296
|
+
var pr: Dictionary = _probes[key]
|
|
297
|
+
var peer: StreamPeerTCP = pr["peer"]
|
|
298
|
+
peer.poll()
|
|
299
|
+
var st := peer.get_status()
|
|
300
|
+
if st == StreamPeerTCP.STATUS_CONNECTED:
|
|
301
|
+
_set_plane(key, "ok", "127.0.0.1:%d reachable" % int(pr["port"]))
|
|
302
|
+
peer.disconnect_from_host()
|
|
303
|
+
done.append(key)
|
|
304
|
+
elif st == StreamPeerTCP.STATUS_ERROR:
|
|
305
|
+
_set_plane(key, "fail", "127.0.0.1:%d unreachable" % int(pr["port"]))
|
|
306
|
+
done.append(key)
|
|
307
|
+
elif Time.get_ticks_msec() > int(pr["deadline"]):
|
|
308
|
+
_set_plane(key, "fail", "127.0.0.1:%d no response" % int(pr["port"]))
|
|
309
|
+
peer.disconnect_from_host()
|
|
310
|
+
done.append(key)
|
|
311
|
+
for key in done:
|
|
312
|
+
_probes.erase(key)
|
package/dist/index.js
CHANGED
|
@@ -43,7 +43,7 @@ async function main() {
|
|
|
43
43
|
// so long jobs (export/import/headless script) support poll/await/cancel.
|
|
44
44
|
// D3: also advertise resources.subscribe so clients can subscribe to
|
|
45
45
|
// godot://… resources and receive notifications/resources/updated.
|
|
46
|
-
const server = new McpServer({ name: "breakpoint-mcp", version: "1.
|
|
46
|
+
const server = new McpServer({ name: "breakpoint-mcp", version: "1.4.0" }, { capabilities: { ...TASK_CAPABILITIES, ...RESOURCE_CAPABILITIES }, taskStore });
|
|
47
47
|
// B1: enforce frozen output schemas on every structured tool. Must run before
|
|
48
48
|
// the register*Tools calls below — it wraps server.registerTool.
|
|
49
49
|
applyOutputSchemas(server);
|
package/dist/schemas.js
CHANGED
|
@@ -326,6 +326,22 @@ export const outputSchemas = {
|
|
|
326
326
|
red: z.number(), green: z.number(), blue: z.number(), alpha: z.number(), hex: z.string(),
|
|
327
327
|
})),
|
|
328
328
|
},
|
|
329
|
+
gd_call_hierarchy: {
|
|
330
|
+
direction: z.string(),
|
|
331
|
+
items: z.array(z.object({
|
|
332
|
+
name: z.string(), kind: z.string(), uri: z.string(), line: z.number(), character: z.number(), detail: z.string(),
|
|
333
|
+
calls: z.array(z.object({
|
|
334
|
+
name: z.string(), kind: z.string(), uri: z.string(), line: z.number(), character: z.number(), detail: z.string(),
|
|
335
|
+
ranges: z.array(z.object({ line: z.number(), character: z.number(), end_line: z.number(), end_character: z.number() })),
|
|
336
|
+
})),
|
|
337
|
+
})),
|
|
338
|
+
},
|
|
339
|
+
gd_semantic_tokens: {
|
|
340
|
+
token_count: z.number(),
|
|
341
|
+
tokens: z.array(z.object({
|
|
342
|
+
line: z.number(), character: z.number(), length: z.number(), type: z.string(), modifiers: z.array(z.string()),
|
|
343
|
+
})),
|
|
344
|
+
},
|
|
329
345
|
// ---- Plane D: C# semantic / OmniSharp LSP (tools/cslsp.ts) ----
|
|
330
346
|
cs_completion: { items: z.array(z.object({ label: z.string(), kind: z.string(), detail: z.string(), insertText: z.string() })) },
|
|
331
347
|
cs_hover: { contents: z.string() },
|
package/dist/tools/lsp.js
CHANGED
|
@@ -117,6 +117,52 @@ function normalizeColors(result) {
|
|
|
117
117
|
};
|
|
118
118
|
});
|
|
119
119
|
}
|
|
120
|
+
// textDocument/prepareCallHierarchy + callHierarchy/{incoming,outgoing}Calls.
|
|
121
|
+
// A CallHierarchyItem carries a SymbolKind and a selectionRange (the name range);
|
|
122
|
+
// surface the selectionRange start (falling back to the full range) so a caller
|
|
123
|
+
// gets the caret position of each symbol.
|
|
124
|
+
function normalizeCallItem(item) {
|
|
125
|
+
const it = (item ?? {});
|
|
126
|
+
const r = it.selectionRange ?? it.range ?? {};
|
|
127
|
+
return {
|
|
128
|
+
name: it.name ?? "",
|
|
129
|
+
kind: it.kind ? SYMBOL_KIND[it.kind] ?? String(it.kind) : "",
|
|
130
|
+
uri: it.uri ?? "",
|
|
131
|
+
line: r.start?.line ?? 0,
|
|
132
|
+
character: r.start?.character ?? 0,
|
|
133
|
+
detail: it.detail ?? "",
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
// The call-site ranges (fromRanges) an incoming/outgoing call reports, within the
|
|
137
|
+
// file that makes the call.
|
|
138
|
+
function normalizeCallRanges(ranges) {
|
|
139
|
+
const arr = Array.isArray(ranges) ? ranges : [];
|
|
140
|
+
return arr.map((rr) => {
|
|
141
|
+
const r = (rr ?? {});
|
|
142
|
+
return {
|
|
143
|
+
line: r.start?.line ?? 0, character: r.start?.character ?? 0,
|
|
144
|
+
end_line: r.end?.line ?? 0, end_character: r.end?.character ?? 0,
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
function decodeSemanticTokens(data, legend) {
|
|
149
|
+
const d = Array.isArray(data) ? data : [];
|
|
150
|
+
const types = legend?.tokenTypes ?? [];
|
|
151
|
+
const mods = legend?.tokenModifiers ?? [];
|
|
152
|
+
const out = [];
|
|
153
|
+
let line = 0, char = 0;
|
|
154
|
+
for (let i = 0; i + 4 < d.length; i += 5) {
|
|
155
|
+
const dLine = d[i] ?? 0, dChar = d[i + 1] ?? 0, len = d[i + 2] ?? 0, typeIdx = d[i + 3] ?? 0, modBits = d[i + 4] ?? 0;
|
|
156
|
+
line += dLine;
|
|
157
|
+
char = dLine === 0 ? char + dChar : dChar;
|
|
158
|
+
const modifiers = [];
|
|
159
|
+
for (let b = 0; b < 32; b++)
|
|
160
|
+
if (modBits & (1 << b))
|
|
161
|
+
modifiers.push(mods[b] ?? String(b));
|
|
162
|
+
out.push({ line, character: char, length: len, type: types[typeIdx] ?? String(typeIdx), modifiers });
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
120
166
|
// offsetOf / applyTextEdits now live in ./lsp-common.js (shared with the C#
|
|
121
167
|
// rename mutator, cs_rename). Imported above.
|
|
122
168
|
export function registerLspTools(server, lsp, cfg) {
|
|
@@ -572,5 +618,72 @@ export function registerLspTools(server, lsp, cfg) {
|
|
|
572
618
|
return fail(err);
|
|
573
619
|
}
|
|
574
620
|
});
|
|
621
|
+
// ---- Phase 2 LSP-depth: call hierarchy + semantic tokens -----------------
|
|
622
|
+
// Two heavier semantic providers. Godot's GDScript language server does not
|
|
623
|
+
// advertise callHierarchyProvider or semanticTokensProvider on current builds
|
|
624
|
+
// (observed through 4.7), so both feature-detect and return a clear
|
|
625
|
+
// "unsupported" message, keeping the D7 -32601 belt-and-suspenders in case a
|
|
626
|
+
// future build advertises one but still answers "method not found".
|
|
627
|
+
server.registerTool("gd_call_hierarchy", {
|
|
628
|
+
title: "GDScript call hierarchy",
|
|
629
|
+
description: "Find the callers (direction=incoming, the default) or callees (direction=outgoing) of the function at a position. " +
|
|
630
|
+
"Resolves the symbol with textDocument/prepareCallHierarchy, then callHierarchy/incomingCalls or outgoingCalls, and " +
|
|
631
|
+
"returns each related function with the call-site ranges. Read-only. " +
|
|
632
|
+
"Godot's GDScript language server does not advertise callHierarchyProvider through 4.7; feature-detected.",
|
|
633
|
+
inputSchema: {
|
|
634
|
+
...posSchema,
|
|
635
|
+
direction: z.enum(["incoming", "outgoing"]).optional().describe("incoming = who calls this (default); outgoing = what this calls"),
|
|
636
|
+
},
|
|
637
|
+
}, async ({ path, line, character, direction }) => {
|
|
638
|
+
const dir = direction ?? "incoming";
|
|
639
|
+
const alt = "Use gd_references to find where a symbol is used, or gd_definition to jump to it.";
|
|
640
|
+
try {
|
|
641
|
+
const caps = await lsp.getServerCapabilities();
|
|
642
|
+
if (!caps.callHierarchyProvider)
|
|
643
|
+
return unsupportedLsp("gd_call_hierarchy", "textDocument/prepareCallHierarchy", "callHierarchyProvider", alt);
|
|
644
|
+
const uri = await openAndPos(path);
|
|
645
|
+
const prepared = (await lsp.request("textDocument/prepareCallHierarchy", { textDocument: { uri }, position: { line, character } }));
|
|
646
|
+
const method = dir === "outgoing" ? "callHierarchy/outgoingCalls" : "callHierarchy/incomingCalls";
|
|
647
|
+
const items = await Promise.all((prepared ?? []).map(async (prep) => {
|
|
648
|
+
const calls = (await lsp.request(method, { item: prep }));
|
|
649
|
+
const related = (calls ?? []).map((c) => {
|
|
650
|
+
const cc = (c ?? {});
|
|
651
|
+
const peer = dir === "outgoing" ? cc.to : cc.from;
|
|
652
|
+
return { ...normalizeCallItem(peer), ranges: normalizeCallRanges(cc.fromRanges) };
|
|
653
|
+
});
|
|
654
|
+
return { ...normalizeCallItem(prep), calls: related };
|
|
655
|
+
}));
|
|
656
|
+
return ok({ direction: dir, items });
|
|
657
|
+
}
|
|
658
|
+
catch (err) {
|
|
659
|
+
if (isMethodNotFound(err))
|
|
660
|
+
return unsupportedLsp("gd_call_hierarchy", "textDocument/prepareCallHierarchy", "callHierarchyProvider", alt);
|
|
661
|
+
return fail(err);
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
server.registerTool("gd_semantic_tokens", {
|
|
665
|
+
title: "GDScript semantic tokens",
|
|
666
|
+
description: "Return the semantic-highlighting tokens for a whole script — each token's position, length, type (e.g. function, " +
|
|
667
|
+
"variable, keyword) and modifiers — decoded from the LSP packed-integer form through the server's advertised legend. " +
|
|
668
|
+
"Read-only. Godot's GDScript language server does not advertise semanticTokensProvider through 4.7; feature-detected.",
|
|
669
|
+
inputSchema: { path: z.string().describe("Script path (res://..., absolute, or project-relative)") },
|
|
670
|
+
}, async ({ path }) => {
|
|
671
|
+
const alt = "Use gd_document_symbols for a structural outline of the file.";
|
|
672
|
+
try {
|
|
673
|
+
const caps = await lsp.getServerCapabilities();
|
|
674
|
+
const provider = caps.semanticTokensProvider;
|
|
675
|
+
if (!provider)
|
|
676
|
+
return unsupportedLsp("gd_semantic_tokens", "textDocument/semanticTokens/full", "semanticTokensProvider", alt);
|
|
677
|
+
const uri = await openAndPos(path);
|
|
678
|
+
const result = (await lsp.request("textDocument/semanticTokens/full", { textDocument: { uri } }));
|
|
679
|
+
const tokens = decodeSemanticTokens(result?.data ?? [], provider.legend);
|
|
680
|
+
return ok({ token_count: tokens.length, tokens });
|
|
681
|
+
}
|
|
682
|
+
catch (err) {
|
|
683
|
+
if (isMethodNotFound(err))
|
|
684
|
+
return unsupportedLsp("gd_semantic_tokens", "textDocument/semanticTokens/full", "semanticTokensProvider", alt);
|
|
685
|
+
return fail(err);
|
|
686
|
+
}
|
|
687
|
+
});
|
|
575
688
|
}
|
|
576
689
|
//# sourceMappingURL=lsp.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "breakpoint-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "MCP server exposing Godot to AI coding assistants over the Model Context Protocol — developed and tested with Claude — across four planes (CLI, live editor, LSP+DAP, runtime) with elicitation-gated destructive tools, long jobs on the formal MCP task model, captured console output, and MCP resources.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|