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.
@@ -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.72.7" }, { capabilities: { ...TASK_CAPABILITIES, ...RESOURCE_CAPABILITIES }, taskStore });
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.72.7",
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
  }