breakpoint-mcp 1.1.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.
@@ -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