breakpoint-mcp 1.1.0 → 1.2.1

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 CHANGED
@@ -15,10 +15,28 @@ architecture.
15
15
 
16
16
  ## Install
17
17
 
18
+ The fastest path — from your Godot project folder, one command installs and enables the
19
+ editor addon (bundled in this package) and prints your MCP-client config:
20
+
21
+ ```bash
22
+ npx breakpoint-mcp init
23
+ ```
24
+
25
+ Then open the project in Godot and verify the setup:
26
+
27
+ ```bash
28
+ npx breakpoint-mcp doctor
29
+ ```
30
+
31
+ `init` copies the addon into `addons/breakpoint_mcp/`, enables it in `project.godot`, and
32
+ prints — or, with `--client claude-code|claude-desktop|cursor|windsurf|vscode`, writes —
33
+ the client config. `doctor` checks the Godot binary, the addon, and the four bridges
34
+ (add `--require-live` once the editor is open; `--json` for a machine-readable report).
35
+
36
+ To install the `breakpoint-mcp` command globally instead of invoking it via `npx`:
37
+
18
38
  ```bash
19
- npx breakpoint-mcp # run on demand
20
- # or
21
- npm i -g breakpoint-mcp # install the `breakpoint-mcp` command
39
+ npm i -g breakpoint-mcp
22
40
  ```
23
41
 
24
42
  Requires **Node ≥ 18**. The host targets the `@modelcontextprotocol/sdk` `1.x` line
@@ -60,10 +78,11 @@ section for each client's config file and format.
60
78
 
61
79
  ## The addon (required for the editor / runtime planes)
62
80
 
63
- Install the `breakpoint_mcp` editor addon into your Godot project (drop
64
- `addons/breakpoint_mcp/` in and enable it under Project Settings Plugins). It opens the
65
- loopback servers this host connects to and auto-registers the in-game runtime bridge.
66
- Without it, only the headless-CLI (`godot_*`) plane works.
81
+ `breakpoint-mcp init` installs and enables this for you — the editor addon ships **inside
82
+ this package**, so you don't need the repository to get it. To do it by hand instead, copy
83
+ `addons/breakpoint_mcp/` into your project and enable it under Project Settings Plugins.
84
+ Either way it opens the loopback servers this host connects to and auto-registers the
85
+ in-game runtime bridge; without it, only the headless-CLI (`godot_*`) plane works.
67
86
 
68
87
  ## Local-first
69
88
 
@@ -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