breakpoint-mcp 1.0.0 → 1.2.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/addon/breakpoint_mcp/README.md +28 -0
- package/addon/breakpoint_mcp/bridge_server.gd +121 -0
- package/addon/breakpoint_mcp/icon.png +0 -0
- package/addon/breakpoint_mcp/operations.gd +4578 -0
- package/addon/breakpoint_mcp/plugin.cfg +7 -0
- package/addon/breakpoint_mcp/plugin.gd +64 -0
- package/addon/breakpoint_mcp/runtime_bridge.gd +440 -0
- package/addon/breakpoint_mcp/variant_json.gd +108 -0
- package/dist/cli/args.js +40 -0
- package/dist/cli/clients.js +74 -0
- package/dist/cli/doctor.js +269 -0
- package/dist/cli/init.js +199 -0
- package/dist/config.js +6 -30
- package/dist/index.js +49 -4
- package/dist/tools/editor/animation.js +162 -0
- package/dist/tools/editor/audio.js +109 -0
- package/dist/tools/editor/common.js +31 -0
- package/dist/tools/editor/core.js +49 -0
- package/dist/tools/editor/filesystem.js +35 -0
- package/dist/tools/editor/introspection.js +73 -0
- package/dist/tools/editor/node.js +171 -0
- package/dist/tools/editor/particles.js +96 -0
- package/dist/tools/editor/physics.js +236 -0
- package/dist/tools/editor/project_input_test.js +187 -0
- package/dist/tools/editor/resource.js +104 -0
- package/dist/tools/editor/scene.js +95 -0
- package/dist/tools/editor/shader.js +59 -0
- package/dist/tools/editor/signal.js +67 -0
- package/dist/tools/editor/spatial.js +185 -0
- package/dist/tools/editor/tiles.js +160 -0
- package/dist/tools/editor/ui.js +175 -0
- package/dist/tools/editor.js +39 -1914
- package/package.json +4 -2
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Breakpoint MCP (Godot editor addon)
|
|
2
|
+
|
|
3
|
+
Loopback TCP/JSON bridge that exposes the live Godot editor (and, via the runtime autoload, the running game) to an MCP host so an AI assistant can drive it. Part of [breakpoint-mcp](../../../README.md).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
1. Copy this `breakpoint_mcp/` folder into your project's `addons/` directory so the path is `res://addons/breakpoint_mcp/`.
|
|
7
|
+
2. Enable it under **Project → Project Settings → Plugins → Breakpoint MCP**.
|
|
8
|
+
3. On enable it listens on `127.0.0.1:9080`. Override the port by setting the `BREAKPOINT_BRIDGE_PORT` environment variable *before* launching Godot.
|
|
9
|
+
|
|
10
|
+
Requires **Godot 4.2+** (uses the `EditorInterface` singleton; 4.4+ recommended).
|
|
11
|
+
|
|
12
|
+
## Files
|
|
13
|
+
- `plugin.gd` — `EditorPlugin` entry point; starts/stops the server.
|
|
14
|
+
- `bridge_server.gd` — `TCPServer` polled from `_process`; newline-delimited JSON framing.
|
|
15
|
+
- `operations.gd` — request handlers; every mutation is wrapped in `EditorUndoRedoManager`.
|
|
16
|
+
- `variant_json.gd` — Variant ⇄ JSON codec (tagged rich types).
|
|
17
|
+
|
|
18
|
+
## Protocol
|
|
19
|
+
One JSON object per line (`\n`-terminated), both directions:
|
|
20
|
+
```
|
|
21
|
+
→ {"id":"<string>","method":"node.add","params":{"parent_path":".","type":"Sprite2D"}}
|
|
22
|
+
← {"id":"<string>","ok":true,"result":{"path":"Sprite2D","name":"Sprite2D","type":"Sprite2D"}}
|
|
23
|
+
← {"id":"<string>","ok":false,"error":{"code":"no_scene","message":"No scene is open"}}
|
|
24
|
+
```
|
|
25
|
+
Method names and payloads correspond 1:1 to the `editor_*` / `scene_*` / `node_*` tools in [`docs/TOOL_CATALOG.md`](../../../docs/TOOL_CATALOG.md).
|
|
26
|
+
|
|
27
|
+
## Security
|
|
28
|
+
Binds to loopback only. Handlers execute on the editor main thread. Treat the port as a local trust boundary — do not expose it beyond `127.0.0.1`.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
@tool
|
|
2
|
+
extends Node
|
|
3
|
+
## Loopback TCP server speaking newline-delimited JSON-RPC-ish messages.
|
|
4
|
+
##
|
|
5
|
+
## Wire format (both directions), one JSON object per line ("\n" terminated):
|
|
6
|
+
## request: {"id": "<string>", "method": "<name>", "params": { ... }}
|
|
7
|
+
## response: {"id": "<string>", "ok": true, "result": { ... }}
|
|
8
|
+
## {"id": "<string>", "ok": false, "error": {"code": ..., "message": ...}}
|
|
9
|
+
##
|
|
10
|
+
## Binds to 127.0.0.1 only. The port comes from the BREAKPOINT_BRIDGE_PORT env
|
|
11
|
+
## var (default 9080). The socket is polled from `_process`, so all request
|
|
12
|
+
## handlers run on the editor's main thread.
|
|
13
|
+
|
|
14
|
+
const Operations := preload("res://addons/breakpoint_mcp/operations.gd")
|
|
15
|
+
const DEFAULT_PORT := 9080
|
|
16
|
+
|
|
17
|
+
var _server: TCPServer
|
|
18
|
+
var _ops: Operations
|
|
19
|
+
var _clients: Array = [] # Array of {peer: StreamPeerTCP, buf: String}
|
|
20
|
+
var _port: int = DEFAULT_PORT
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
func setup(plugin: EditorPlugin) -> void:
|
|
24
|
+
_ops = Operations.new()
|
|
25
|
+
_ops.setup(plugin)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
func _ready() -> void:
|
|
29
|
+
var env_port := OS.get_environment("BREAKPOINT_BRIDGE_PORT")
|
|
30
|
+
if env_port != "" and env_port.is_valid_int():
|
|
31
|
+
_port = int(env_port)
|
|
32
|
+
_server = TCPServer.new()
|
|
33
|
+
var err := _server.listen(_port, "127.0.0.1")
|
|
34
|
+
if err != OK:
|
|
35
|
+
push_error("[breakpoint_mcp] could not listen on 127.0.0.1:%d (error %d)" % [_port, err])
|
|
36
|
+
else:
|
|
37
|
+
print("[breakpoint_mcp] listening on 127.0.0.1:%d" % _port)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
func shutdown() -> void:
|
|
41
|
+
for c in _clients:
|
|
42
|
+
var peer: StreamPeerTCP = c["peer"]
|
|
43
|
+
if peer:
|
|
44
|
+
peer.disconnect_from_host()
|
|
45
|
+
_clients.clear()
|
|
46
|
+
if _server:
|
|
47
|
+
_server.stop()
|
|
48
|
+
_server = null
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
func _process(_delta: float) -> void:
|
|
52
|
+
if _server == null:
|
|
53
|
+
return
|
|
54
|
+
# Accept new connections.
|
|
55
|
+
while _server.is_connection_available():
|
|
56
|
+
var peer := _server.take_connection()
|
|
57
|
+
if peer:
|
|
58
|
+
_clients.append({"peer": peer, "buf": ""})
|
|
59
|
+
# Service existing connections.
|
|
60
|
+
var still_alive: Array = []
|
|
61
|
+
for c in _clients:
|
|
62
|
+
var peer: StreamPeerTCP = c["peer"]
|
|
63
|
+
peer.poll()
|
|
64
|
+
var status := peer.get_status()
|
|
65
|
+
if status == StreamPeerTCP.STATUS_ERROR or status == StreamPeerTCP.STATUS_NONE:
|
|
66
|
+
continue
|
|
67
|
+
var available := peer.get_available_bytes()
|
|
68
|
+
if available > 0:
|
|
69
|
+
var chunk := peer.get_data(available)
|
|
70
|
+
if chunk[0] == OK:
|
|
71
|
+
var bytes: PackedByteArray = chunk[1]
|
|
72
|
+
c["buf"] += bytes.get_string_from_utf8()
|
|
73
|
+
_drain_lines(c)
|
|
74
|
+
still_alive.append(c)
|
|
75
|
+
_clients = still_alive
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
func _drain_lines(c: Dictionary) -> void:
|
|
79
|
+
var buf: String = c["buf"]
|
|
80
|
+
while true:
|
|
81
|
+
var nl := buf.find("\n")
|
|
82
|
+
if nl == -1:
|
|
83
|
+
break
|
|
84
|
+
var line := buf.substr(0, nl)
|
|
85
|
+
buf = buf.substr(nl + 1)
|
|
86
|
+
line = line.strip_edges()
|
|
87
|
+
if line != "":
|
|
88
|
+
_handle_line(c["peer"], line)
|
|
89
|
+
c["buf"] = buf
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
func _handle_line(peer: StreamPeerTCP, line: String) -> void:
|
|
93
|
+
var parsed: Variant = JSON.parse_string(line)
|
|
94
|
+
if typeof(parsed) != TYPE_DICTIONARY:
|
|
95
|
+
_send(peer, {"id": null, "ok": false, "error": {"code": "bad_json", "message": "Could not parse request line"}})
|
|
96
|
+
return
|
|
97
|
+
var req: Dictionary = parsed
|
|
98
|
+
var id: Variant = req.get("id", null)
|
|
99
|
+
var method := String(req.get("method", ""))
|
|
100
|
+
var params: Dictionary = req.get("params", {}) if typeof(req.get("params")) == TYPE_DICTIONARY else {}
|
|
101
|
+
var result: Dictionary
|
|
102
|
+
# Handlers never throw in normal operation; guard anyway.
|
|
103
|
+
result = _ops.dispatch(method, params)
|
|
104
|
+
var response := {"id": id}
|
|
105
|
+
response.merge(result)
|
|
106
|
+
_send(peer, response)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
func _send(peer: StreamPeerTCP, obj: Dictionary) -> void:
|
|
110
|
+
var text := JSON.stringify(obj) + "\n"
|
|
111
|
+
peer.put_data(text.to_utf8_buffer())
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
## D3: push an unsolicited "resource changed" event to every connected client
|
|
115
|
+
## so a subscribed MCP host can emit notifications/resources/updated. Events carry
|
|
116
|
+
## no "id" (they are not responses); the host routes them by the "event" field.
|
|
117
|
+
func broadcast_event(uri: String) -> void:
|
|
118
|
+
for c in _clients:
|
|
119
|
+
var peer: StreamPeerTCP = c["peer"]
|
|
120
|
+
if peer and peer.get_status() == StreamPeerTCP.STATUS_CONNECTED:
|
|
121
|
+
_send(peer, {"event": "resource.changed", "uri": uri})
|
|
Binary file
|