breakpoint-mcp 1.72.7 → 1.73.2
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/runtime_bridge.gd +65 -0
- package/dist/index.js +11 -1
- package/dist/schemas.js +46 -22
- package/dist/wire-defaults.js +161 -0
- package/package.json +2 -1
|
@@ -14,6 +14,11 @@ extends Node
|
|
|
14
14
|
const Codec := preload("res://addons/breakpoint_mcp/variant_json.gd")
|
|
15
15
|
const DEFAULT_PORT := 9081
|
|
16
16
|
const LOG_CAP := 1000
|
|
17
|
+
# D1a: how many error/warning entries one response may carry. A cap and not the
|
|
18
|
+
# whole ring: a call that provokes a thousand warnings must not turn a 200-byte
|
|
19
|
+
# reply into a 100 KB one. The `total` beside the list is what stops the cap
|
|
20
|
+
# lying about how many there were.
|
|
21
|
+
const ENGINE_LOG_CAP := 20
|
|
17
22
|
# D6: source for the runtime-compiled Logger subclass (Godot 4.5+). Kept as a
|
|
18
23
|
# string so `extends Logger` is only ever compiled where the class exists — the
|
|
19
24
|
# addon stays parse-clean on Godot 4.3/4.4 (no Logger class).
|
|
@@ -235,9 +240,17 @@ func _handle_line(c: Dictionary, line: String) -> void:
|
|
|
235
240
|
if method in ASYNC_METHODS:
|
|
236
241
|
_dispatch_async(c, id, method, params)
|
|
237
242
|
return
|
|
243
|
+
# D1a: the engine-error echo. The ring already receives every push_error /
|
|
244
|
+
# push_warning and every engine message (via the 4.5+ Logger capture), but
|
|
245
|
+
# nothing tied one to the CALL that provoked it — a caller had to notice
|
|
246
|
+
# something was wrong, then go and read runtime.get_log, then guess which
|
|
247
|
+
# entries were its own. Reading _log_seq either side of the dispatch answers
|
|
248
|
+
# that by construction: everything appended in between belongs to this call.
|
|
249
|
+
var seq_before := _log_seq
|
|
238
250
|
var result := _dispatch(method, params)
|
|
239
251
|
var response := {"id": id}
|
|
240
252
|
response.merge(result)
|
|
253
|
+
_attach_engine_log(response, seq_before)
|
|
241
254
|
_send(peer, response)
|
|
242
255
|
|
|
243
256
|
|
|
@@ -306,6 +319,51 @@ func _on_captured_log(level: String, message: String) -> void:
|
|
|
306
319
|
_in_capture = false
|
|
307
320
|
|
|
308
321
|
|
|
322
|
+
## D1a: every `error`/`warning` ring entry appended AFTER `seq_before`, i.e. during
|
|
323
|
+
## the dispatch this response belongs to. Written into `result` and not beside it:
|
|
324
|
+
## the host resolves a reply as `msg.result`, so a sibling key is dropped on the
|
|
325
|
+
## floor — and `structuredContent` must match the tool's `outputSchema` or a
|
|
326
|
+
## conforming client throws, which is why `engine_log` is declared on all 26
|
|
327
|
+
## runtime output schemas rather than smuggled in.
|
|
328
|
+
##
|
|
329
|
+
## `isError` IS DELIBERATELY UNTOUCHED. A push_error during a call that returned
|
|
330
|
+
## what it was asked for is a DIAGNOSTIC, not a failed call; promoting it would
|
|
331
|
+
## make every noisy frame look like a broken tool and would change the meaning of
|
|
332
|
+
## a field callers already branch on.
|
|
333
|
+
##
|
|
334
|
+
## Absent when there is nothing, never `[]`: an optional field that is always
|
|
335
|
+
## present carries no information, and 208 §4's finding is that absent and
|
|
336
|
+
## explicitly-empty are NOT the same value to a client that never materialises
|
|
337
|
+
## defaults.
|
|
338
|
+
func _attach_engine_log(response: Dictionary, seq_before: int) -> void:
|
|
339
|
+
if not response.get("ok", false):
|
|
340
|
+
return # a failed call already carries its own error; see the note above
|
|
341
|
+
var result: Variant = response.get("result")
|
|
342
|
+
if typeof(result) != TYPE_DICTIONARY:
|
|
343
|
+
return
|
|
344
|
+
var entries: Array = []
|
|
345
|
+
for e in _log:
|
|
346
|
+
if entries.size() >= ENGINE_LOG_CAP:
|
|
347
|
+
break
|
|
348
|
+
if int(e.get("seq", 0)) <= seq_before:
|
|
349
|
+
continue
|
|
350
|
+
var lvl := String(e.get("level", ""))
|
|
351
|
+
if lvl == "error" or lvl == "warning":
|
|
352
|
+
entries.append(e)
|
|
353
|
+
if entries.is_empty():
|
|
354
|
+
return
|
|
355
|
+
# 🔴 THE COUNT IS SEPARATE FROM THE LIST, because the list is CAPPED. A caller
|
|
356
|
+
# reading twenty entries cannot tell "twenty happened" from "hundreds happened
|
|
357
|
+
# and you are seeing the first twenty", and the second is the case that matters.
|
|
358
|
+
var total := 0
|
|
359
|
+
for e in _log:
|
|
360
|
+
if int(e.get("seq", 0)) > seq_before:
|
|
361
|
+
var lvl2 := String(e.get("level", ""))
|
|
362
|
+
if lvl2 == "error" or lvl2 == "warning":
|
|
363
|
+
total += 1
|
|
364
|
+
result["engine_log"] = {"entries": entries, "total": total, "since_seq": seq_before}
|
|
365
|
+
|
|
366
|
+
|
|
309
367
|
# ----------------------------------------------------------- dispatch --------
|
|
310
368
|
|
|
311
369
|
func _dispatch(method: String, params: Dictionary) -> Dictionary:
|
|
@@ -972,6 +1030,12 @@ func _node_remove(params: Dictionary) -> Dictionary:
|
|
|
972
1030
|
## as a bare (un-awaited) call from _handle_line so _process never blocks; it resumes on the
|
|
973
1031
|
## engine's frame signals and sends when complete.
|
|
974
1032
|
func _dispatch_async(c: Dictionary, id: Variant, method: String, params: Dictionary) -> void:
|
|
1033
|
+
# D1a: the async lane echoes too, and it is the lane that needs it MOST — a
|
|
1034
|
+
# step_frames call runs the game for N frames, so it is the one dispatch here
|
|
1035
|
+
# that can provoke errors from code the caller never named. Handing it the
|
|
1036
|
+
# synchronous version's treatment is not symmetry for its own sake: a caller
|
|
1037
|
+
# stepping frames to reproduce a bug is exactly the caller this feature is for.
|
|
1038
|
+
var seq_before := _log_seq
|
|
975
1039
|
var result: Dictionary
|
|
976
1040
|
match method:
|
|
977
1041
|
"runtime.step_frames":
|
|
@@ -982,6 +1046,7 @@ func _dispatch_async(c: Dictionary, id: Variant, method: String, params: Diction
|
|
|
982
1046
|
if peer != null and peer.get_status() == StreamPeerTCP.STATUS_CONNECTED:
|
|
983
1047
|
var response := {"id": id}
|
|
984
1048
|
response.merge(result)
|
|
1049
|
+
_attach_engine_log(response, seq_before)
|
|
985
1050
|
_send(peer, response)
|
|
986
1051
|
|
|
987
1052
|
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { StdioChannel } from "./stdio.js";
|
|
|
11
11
|
import { DapClient } from "./dap.js";
|
|
12
12
|
import { buildToolsets } from "./toolsets.js";
|
|
13
13
|
import { registerRecipes } from "./recipes.js";
|
|
14
|
+
import { applyWireDefaults } from "./wire-defaults.js";
|
|
14
15
|
import { applyOutputSchemas } from "./schemas.js";
|
|
15
16
|
import { applyAnnotations } from "./annotations.js";
|
|
16
17
|
import { applyTimeoutCaveat } from "./timeout-caveat.js";
|
|
@@ -41,7 +42,16 @@ async function main() {
|
|
|
41
42
|
// so long jobs (export/import/headless script) support poll/await/cancel.
|
|
42
43
|
// D3: also advertise resources.subscribe so clients can subscribe to
|
|
43
44
|
// godot://… resources and receive notifications/resources/updated.
|
|
44
|
-
const server = new McpServer({ name: "breakpoint-mcp", version: "1.
|
|
45
|
+
const server = new McpServer({ name: "breakpoint-mcp", version: "1.73.2" }, { capabilities: { ...TASK_CAPABILITIES, ...RESOURCE_CAPABILITIES }, taskStore });
|
|
46
|
+
// 208 §7.1 — delete from the wire what the protocol already says: the SDK's
|
|
47
|
+
// draft-07 `$schema` inside every input/output schema (MCP's default dialect is
|
|
48
|
+
// 2020-12, and declaring another is an explicit switch a peer may reject), and
|
|
49
|
+
// `execution: {taskSupport:"forbidden"}` on the 288 non-task tools (the spec's own
|
|
50
|
+
// value for an ABSENT field, in a field revision 2026-07-28 deletes outright).
|
|
51
|
+
// Neither is authored here; both are SDK output. 🔴 FIRST of the apply* family and
|
|
52
|
+
// not merely early: McpServer installs its tools/list handler from inside the FIRST
|
|
53
|
+
// registerTool, so a wrapper added after any registration wraps nothing.
|
|
54
|
+
applyWireDefaults(server);
|
|
45
55
|
// B1: enforce frozen output schemas on every structured tool. Must run before
|
|
46
56
|
// any register*Tools call — it wraps server.registerTool.
|
|
47
57
|
applyOutputSchemas(server);
|
package/dist/schemas.js
CHANGED
|
@@ -44,6 +44,30 @@ const runtimeNode = z.lazy(() => z.object({
|
|
|
44
44
|
visible: z.boolean().optional(),
|
|
45
45
|
children: z.array(runtimeNode).optional(),
|
|
46
46
|
}));
|
|
47
|
+
/**
|
|
48
|
+
* D1a — the engine-error echo, declared once and spread into the 22 runtime
|
|
49
|
+
* tools whose `structuredContent` IS the bridge reply verbatim.
|
|
50
|
+
*
|
|
51
|
+
* 🔴 DECLARED RATHER THAN SMUGGLED. `structuredContent` is validated against
|
|
52
|
+
* `outputSchema` by both SDK clients, which THROW on a mismatch (208 §4), so a
|
|
53
|
+
* field the addon adds and this file does not declare breaks every conforming
|
|
54
|
+
* caller. The blast radius is the price of the feature, not an accident of it.
|
|
55
|
+
*
|
|
56
|
+
* 🔴 AND OPTIONAL, WITH THE ADDON OMITTING IT RATHER THAN SENDING `[]`. 208 §4
|
|
57
|
+
* measured that no SDK materialises a documented default, so absent and
|
|
58
|
+
* explicitly-empty are different values to a client — and "nothing went wrong"
|
|
59
|
+
* is worth saying with silence.
|
|
60
|
+
*/
|
|
61
|
+
const engineLog = {
|
|
62
|
+
engine_log: z
|
|
63
|
+
.object({
|
|
64
|
+
entries: z.array(z.object({ seq: z.number(), level: z.string(), message: z.string() })),
|
|
65
|
+
// The list is CAPPED at 20; this is how many there actually were.
|
|
66
|
+
total: z.number(),
|
|
67
|
+
since_seq: z.number(),
|
|
68
|
+
})
|
|
69
|
+
.optional(),
|
|
70
|
+
};
|
|
47
71
|
/** name -> ZodRawShape describing that tool's structuredContent. */
|
|
48
72
|
export const outputSchemas = {
|
|
49
73
|
// ---- Plane B: headless CLI (tools/cli.ts) ----
|
|
@@ -469,7 +493,7 @@ export const outputSchemas = {
|
|
|
469
493
|
// C# sessions have no scene, so — unlike dbg_restart — no `scene` field here.
|
|
470
494
|
cs_dbg_restart: { session_id: z.string(), method: z.string(), state: z.string() },
|
|
471
495
|
// ---- Plane C: runtime bridge (tools/runtime.ts -> runtime_bridge.gd) ----
|
|
472
|
-
runtime_get_tree: {
|
|
496
|
+
runtime_get_tree: { ...engineLog,
|
|
473
497
|
name: z.string(),
|
|
474
498
|
type: z.string(),
|
|
475
499
|
path: z.string(),
|
|
@@ -477,26 +501,26 @@ export const outputSchemas = {
|
|
|
477
501
|
visible: z.boolean().optional(),
|
|
478
502
|
children: z.array(runtimeNode).optional(),
|
|
479
503
|
},
|
|
480
|
-
runtime_get_property: { path: z.string(), property: z.string(), value: encodedValue },
|
|
481
|
-
runtime_set_property: { path: z.string(), property: z.string(), value: encodedValue },
|
|
482
|
-
runtime_call_method: { return: encodedValue },
|
|
483
|
-
runtime_emit_signal: { emitted: z.boolean() },
|
|
484
|
-
runtime_inject_input: { injected: z.boolean(), kind: z.string() },
|
|
485
|
-
runtime_get_monitors: { monitors: z.record(z.number()) },
|
|
486
|
-
runtime_get_log: {
|
|
504
|
+
runtime_get_property: { ...engineLog, path: z.string(), property: z.string(), value: encodedValue },
|
|
505
|
+
runtime_set_property: { ...engineLog, path: z.string(), property: z.string(), value: encodedValue },
|
|
506
|
+
runtime_call_method: { ...engineLog, return: encodedValue },
|
|
507
|
+
runtime_emit_signal: { ...engineLog, emitted: z.boolean() },
|
|
508
|
+
runtime_inject_input: { ...engineLog, injected: z.boolean(), kind: z.string() },
|
|
509
|
+
runtime_get_monitors: { ...engineLog, monitors: z.record(z.number()) },
|
|
510
|
+
runtime_get_log: { ...engineLog,
|
|
487
511
|
entries: z.array(z.object({ seq: z.number(), level: z.string(), message: z.string() })),
|
|
488
512
|
latest_seq: z.number(),
|
|
489
513
|
// D6: true when the Godot 4.5+ Logger capture is active (zero-config print()
|
|
490
514
|
// capture, no managed parent). Optional so older addons still validate.
|
|
491
515
|
capture: z.boolean().optional(),
|
|
492
516
|
},
|
|
493
|
-
runtime_assert_node_state: {
|
|
517
|
+
runtime_assert_node_state: { ...engineLog,
|
|
494
518
|
path: z.string(),
|
|
495
519
|
ok: z.boolean(),
|
|
496
520
|
checked: z.number(),
|
|
497
521
|
mismatches: z.array(z.object({ property: z.string(), expected: encodedValue, actual: encodedValue })),
|
|
498
522
|
},
|
|
499
|
-
runtime_assert_scene_structure: {
|
|
523
|
+
runtime_assert_scene_structure: { ...engineLog,
|
|
500
524
|
ok: z.boolean(),
|
|
501
525
|
checked: z.number(),
|
|
502
526
|
failures: z.array(z.object({
|
|
@@ -506,7 +530,7 @@ export const outputSchemas = {
|
|
|
506
530
|
actual: encodedValue.optional(),
|
|
507
531
|
})),
|
|
508
532
|
},
|
|
509
|
-
runtime_assert_perf: {
|
|
533
|
+
runtime_assert_perf: { ...engineLog,
|
|
510
534
|
ok: z.boolean(),
|
|
511
535
|
checked: z.number(),
|
|
512
536
|
regressions: z.array(z.object({
|
|
@@ -517,13 +541,13 @@ export const outputSchemas = {
|
|
|
517
541
|
})),
|
|
518
542
|
monitors: z.record(z.number()),
|
|
519
543
|
},
|
|
520
|
-
runtime_assert_screen_text: {
|
|
544
|
+
runtime_assert_screen_text: { ...engineLog,
|
|
521
545
|
ok: z.boolean(),
|
|
522
546
|
matches: z.number(),
|
|
523
547
|
present: z.boolean(),
|
|
524
548
|
samples: z.array(z.object({ path: z.string(), text: z.string() })),
|
|
525
549
|
},
|
|
526
|
-
runtime_screenshot_diff: {
|
|
550
|
+
runtime_screenshot_diff: { ...engineLog,
|
|
527
551
|
ok: z.boolean(),
|
|
528
552
|
diff_ratio: z.number(),
|
|
529
553
|
differing_pixels: z.number(),
|
|
@@ -535,9 +559,9 @@ export const outputSchemas = {
|
|
|
535
559
|
},
|
|
536
560
|
// F8 additions: await/animation/node-lifecycle over the runtime bridge.
|
|
537
561
|
runtime_await_condition: { met: z.boolean(), polls: z.number(), elapsed_ms: z.number(), value: encodedValue },
|
|
538
|
-
runtime_anim_play: { playing: z.boolean(), current_animation: z.string(), speed_scale: z.number() },
|
|
539
|
-
runtime_anim_stop: { playing: z.boolean(), current_animation: z.string(), position: z.number() },
|
|
540
|
-
runtime_anim_get_state: {
|
|
562
|
+
runtime_anim_play: { ...engineLog, playing: z.boolean(), current_animation: z.string(), speed_scale: z.number() },
|
|
563
|
+
runtime_anim_stop: { ...engineLog, playing: z.boolean(), current_animation: z.string(), position: z.number() },
|
|
564
|
+
runtime_anim_get_state: { ...engineLog,
|
|
541
565
|
playing: z.boolean(),
|
|
542
566
|
current_animation: z.string(),
|
|
543
567
|
position: z.number(),
|
|
@@ -545,13 +569,13 @@ export const outputSchemas = {
|
|
|
545
569
|
speed_scale: z.number(),
|
|
546
570
|
animations: z.array(z.string()),
|
|
547
571
|
},
|
|
548
|
-
runtime_node_add: { added: z.boolean(), path: z.string(), type: z.string() },
|
|
549
|
-
runtime_node_remove: { removed: z.boolean(), path: z.string() },
|
|
572
|
+
runtime_node_add: { ...engineLog, added: z.boolean(), path: z.string(), type: z.string() },
|
|
573
|
+
runtime_node_remove: { ...engineLog, removed: z.boolean(), path: z.string() },
|
|
550
574
|
// F4 additions: deterministic playtesting (time control / frame stepping / state digest / RNG seed).
|
|
551
|
-
runtime_time_scale: { previous: z.number(), current: z.number() },
|
|
552
|
-
runtime_step_frames: { frames_advanced: z.number(), frame_index: z.number() },
|
|
553
|
-
runtime_state_digest: { digest: z.record(z.record(encodedValue)), node_count: z.number() },
|
|
554
|
-
runtime_seed_rng: { seed: z.number() },
|
|
575
|
+
runtime_time_scale: { ...engineLog, previous: z.number(), current: z.number() },
|
|
576
|
+
runtime_step_frames: { ...engineLog, frames_advanced: z.number(), frame_index: z.number() },
|
|
577
|
+
runtime_state_digest: { ...engineLog, digest: z.record(z.record(encodedValue)), node_count: z.number() },
|
|
578
|
+
runtime_seed_rng: { ...engineLog, seed: z.number() },
|
|
555
579
|
// F6 additions: multi-peer deterministic playtesting (spawn / stop / converge).
|
|
556
580
|
runtime_spawn_peers: {
|
|
557
581
|
peers: z.array(z.object({
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* 208 §7.1 — WHAT THE PROTOCOL ALREADY SAYS, DELETED FROM THE WIRE.
|
|
4
|
+
*
|
|
5
|
+
* Two fields on every tool in `tools/list` were not authored here and say nothing a
|
|
6
|
+
* conforming peer does not already assume. Both are removed on the way out.
|
|
7
|
+
*
|
|
8
|
+
* 1. 🔴 `"$schema": "http://json-schema.org/draft-07/schema#"`, inside EVERY input and
|
|
9
|
+
* output schema. Nobody in this repo writes it: the SDK converts our Zod through
|
|
10
|
+
* `zod-to-json-schema`, which defaults to draft-07, and offers no target knob on the
|
|
11
|
+
* Zod-v3 path (`server/zod-json-schema-compat.js` — the `target` option is read only
|
|
12
|
+
* by the Zod-v4 branch). MCP fixes the DEFAULT dialect at JSON Schema 2020-12 and
|
|
13
|
+
* requires every implementation to support it; a `$schema` field is an EXPLICIT
|
|
14
|
+
* SWITCH to a dialect nobody is obliged to support and which a peer MUST reject
|
|
15
|
+
* gracefully if it does not. So this is not decoration and it is not a trim:
|
|
16
|
+
* **580 of our 580 schemas fail to compile under a strict 2020-12 validator as
|
|
17
|
+
* shipped, and 580 of 580 compile with the declaration removed, with zero semantic
|
|
18
|
+
* disagreement across 2,320 probe instances.** The bytes (30,160 — 7.7% of the
|
|
19
|
+
* surface) are the smaller half of the finding.
|
|
20
|
+
*
|
|
21
|
+
* 2. `execution: { taskSupport: "forbidden" }`, on the 288 tools that are not task
|
|
22
|
+
* tools. Also not authored here — `McpServer.registerTool` hardcodes it. The spec
|
|
23
|
+
* defines `"forbidden"` as the value when the field is ABSENT ("clients MUST NOT
|
|
24
|
+
* attempt to invoke the tool as a task... This is the default behavior"), and the
|
|
25
|
+
* field does not exist AT ALL on `Tool` in revision 2026-07-28, which moved tasks to
|
|
26
|
+
* an extension with no per-tool flag. The three real task tools keep `"optional"`.
|
|
27
|
+
*
|
|
28
|
+
* 🔴 THE DIALECT STRIP IS FAIL-SAFE BY CONSTRUCTION, AND THAT IS THE WHOLE DESIGN.
|
|
29
|
+
* Removing `$schema` is only meaning-preserving while the schema stays inside the
|
|
30
|
+
* draft-07 ∩ 2020-12 intersection. `dialectSensitive()` walks for the keywords where the
|
|
31
|
+
* two dialects disagree and the declaration is KEPT if it finds one — so a future schema
|
|
32
|
+
* that actually needs draft-07 silently keeps working instead of silently changing
|
|
33
|
+
* meaning. `token-cost.mjs` then refuses on any surviving declaration, which turns the
|
|
34
|
+
* fail-safe from a quiet fallback into a red gate. **The transform never guesses; the
|
|
35
|
+
* gate makes the one case it declines to handle impossible to miss.**
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* Keywords whose FORM or MEANING differs between draft-07 and 2020-12. Presence of any
|
|
39
|
+
* one of them means the dialect label is load-bearing and must not be dropped.
|
|
40
|
+
*
|
|
41
|
+
* 🔴 `exclusiveMinimum`/`exclusiveMaximum` are listed for their BOOLEAN form only —
|
|
42
|
+
* draft-04's. Both draft-07 and 2020-12 take a number there, and 65 of ours do, so
|
|
43
|
+
* matching on the key alone would keep the declaration on a third of the surface for a
|
|
44
|
+
* difference that does not exist. The predicate reads the value, not just the key.
|
|
45
|
+
*/
|
|
46
|
+
export const DIALECT_SENSITIVE = [
|
|
47
|
+
["items", (v) => Array.isArray(v)], // tuple form; 2020-12 spells it prefixItems
|
|
48
|
+
["additionalItems", () => true], // 2020-12 folds this into items
|
|
49
|
+
["exclusiveMinimum", (v) => typeof v === "boolean"], // draft-04 form
|
|
50
|
+
["exclusiveMaximum", (v) => typeof v === "boolean"], // draft-04 form
|
|
51
|
+
["dependencies", () => true], // split into dependentSchemas/dependentRequired
|
|
52
|
+
["definitions", () => true], // $defs in 2020-12
|
|
53
|
+
["$defs", () => true], // 2020-12 only — the label would be wrong already
|
|
54
|
+
["prefixItems", () => true], // 2020-12 only
|
|
55
|
+
["unevaluatedProperties", () => true], // 2020-12 only
|
|
56
|
+
["unevaluatedItems", () => true], // 2020-12 only
|
|
57
|
+
];
|
|
58
|
+
/** Keys under these keywords are AUTHOR-CHOSEN NAMES, not schema vocabulary. */
|
|
59
|
+
const NAME_BEARING = new Set(["properties", "patternProperties", "definitions", "$defs"]);
|
|
60
|
+
/**
|
|
61
|
+
* Does this schema depend on which dialect validates it?
|
|
62
|
+
*
|
|
63
|
+
* 🔴 THE `properties` SKIP IS NOT A REFINEMENT, IT IS THE DIFFERENCE BETWEEN A TRUE AND A
|
|
64
|
+
* FALSE ANSWER. A first pass over this surface reported one `dependencies` hit and would
|
|
65
|
+
* have kept the declaration on the whole surface; the hit was `scene_get_dependencies`'s
|
|
66
|
+
* output field, a property NAMED dependencies. A walker that cannot tell a keyword from a
|
|
67
|
+
* key reads an author's vocabulary as the protocol's.
|
|
68
|
+
*/
|
|
69
|
+
export function dialectSensitive(node, inNamePosition = false) {
|
|
70
|
+
if (node === null || typeof node !== "object")
|
|
71
|
+
return false;
|
|
72
|
+
if (Array.isArray(node))
|
|
73
|
+
return node.some((v) => dialectSensitive(v, false));
|
|
74
|
+
const obj = node;
|
|
75
|
+
// draft-07 ignores keywords adjacent to $ref; 2020-12 applies them.
|
|
76
|
+
if (obj.$ref !== undefined && Object.keys(obj).length > 1)
|
|
77
|
+
return true;
|
|
78
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
79
|
+
if (!inNamePosition) {
|
|
80
|
+
for (const [key, matches] of DIALECT_SENSITIVE)
|
|
81
|
+
if (k === key && matches(v))
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
if (dialectSensitive(v, NAME_BEARING.has(k)))
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
/** The declaration the SDK emits, and the only one this strips. */
|
|
90
|
+
export const SDK_DIALECT = "http://json-schema.org/draft-07/schema#";
|
|
91
|
+
/**
|
|
92
|
+
* Drop `$schema` iff it is the SDK's draft-07 default AND nothing in the schema needs it.
|
|
93
|
+
* Any other declaration is somebody's deliberate choice and is left alone.
|
|
94
|
+
*/
|
|
95
|
+
export function stripDialect(schema) {
|
|
96
|
+
if (schema === null || typeof schema !== "object" || Array.isArray(schema))
|
|
97
|
+
return schema;
|
|
98
|
+
const obj = schema;
|
|
99
|
+
if (obj.$schema !== SDK_DIALECT)
|
|
100
|
+
return schema;
|
|
101
|
+
if (dialectSensitive({ ...obj, $schema: undefined }))
|
|
102
|
+
return schema;
|
|
103
|
+
const { $schema: _dropped, ...rest } = obj;
|
|
104
|
+
return rest;
|
|
105
|
+
}
|
|
106
|
+
/** Drop `execution` iff it carries nothing but the spec's own default. */
|
|
107
|
+
export function stripDefaultExecution(tool) {
|
|
108
|
+
const ex = tool.execution;
|
|
109
|
+
if (ex === null || typeof ex !== "object")
|
|
110
|
+
return tool;
|
|
111
|
+
const keys = Object.keys(ex);
|
|
112
|
+
if (keys.length !== 1 || keys[0] !== "taskSupport")
|
|
113
|
+
return tool;
|
|
114
|
+
if (ex.taskSupport !== "forbidden")
|
|
115
|
+
return tool;
|
|
116
|
+
const { execution: _dropped, ...rest } = tool;
|
|
117
|
+
return rest;
|
|
118
|
+
}
|
|
119
|
+
/** Both transforms over one tool. Pure, so the refusal proofs can drive it directly. */
|
|
120
|
+
export function normalizeTool(tool) {
|
|
121
|
+
let out = tool;
|
|
122
|
+
if (out.inputSchema !== undefined) {
|
|
123
|
+
const s = stripDialect(out.inputSchema);
|
|
124
|
+
if (s !== out.inputSchema)
|
|
125
|
+
out = { ...out, inputSchema: s };
|
|
126
|
+
}
|
|
127
|
+
if (out.outputSchema !== undefined) {
|
|
128
|
+
const s = stripDialect(out.outputSchema);
|
|
129
|
+
if (s !== out.outputSchema)
|
|
130
|
+
out = { ...out, outputSchema: s };
|
|
131
|
+
}
|
|
132
|
+
return stripDefaultExecution(out);
|
|
133
|
+
}
|
|
134
|
+
/** The `tools/list` result, normalized. Shape-preserving: cursors and extras survive. */
|
|
135
|
+
export function normalizeToolList(result) {
|
|
136
|
+
if (!result || !Array.isArray(result.tools))
|
|
137
|
+
return result;
|
|
138
|
+
return { ...result, tools: result.tools.map((t) => normalizeTool(t)) };
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Wrap the low-level `tools/list` handler so every listing leaves normalized.
|
|
142
|
+
*
|
|
143
|
+
* 🔴 MUST RUN BEFORE THE FIRST `registerTool`. `McpServer` installs its tools/list
|
|
144
|
+
* handler lazily, from inside the first registration (`setToolRequestHandlers`), so a
|
|
145
|
+
* wrapper added afterwards has nothing to wrap and would silently do nothing. It is
|
|
146
|
+
* therefore the FIRST of the apply* family, not merely an early one.
|
|
147
|
+
*
|
|
148
|
+
* 🔴 AND IT WRAPS THE HANDLER, NOT THE REGISTRY. The alternative — stripping at
|
|
149
|
+
* registration — cannot work for the dialect: the Zod-to-JSON-Schema conversion happens
|
|
150
|
+
* at LIST time, so at registration there is no `$schema` to remove yet.
|
|
151
|
+
*/
|
|
152
|
+
export function applyWireDefaults(server) {
|
|
153
|
+
const low = server.server;
|
|
154
|
+
const raw = low.setRequestHandler.bind(low);
|
|
155
|
+
low.setRequestHandler = (schema, handler) => {
|
|
156
|
+
if (schema !== ListToolsRequestSchema)
|
|
157
|
+
return raw(schema, handler);
|
|
158
|
+
return raw(schema, async (...args) => normalizeToolList((await handler(...args))));
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=wire-defaults.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "breakpoint-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.73.2",
|
|
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",
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@types/node": "^20.14.0",
|
|
63
|
+
"ajv": "^8.20.0",
|
|
63
64
|
"typescript": "^5.5.0"
|
|
64
65
|
}
|
|
65
66
|
}
|