@csmodding/gameface-devtools-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.
package/README.md CHANGED
@@ -18,6 +18,12 @@ and set JS breakpoints.
18
18
  > `Target.attachedToTarget` is never emitted), so they connect but find zero drivable pages.
19
19
  > A direct CDP client only sends the commands Gameface supports, and works end-to-end.
20
20
 
21
+ > **Why not Coherent Labs' own Gameface MCP?** Theirs is built and documented around a `Player.exe`
22
+ > from a licensed Gameface SDK, and targets Cohtml 3.1.2+. This one attaches to a **game that is
23
+ > already running** — no SDK, no licence, no version floor, field-verified down to Cohtml 1.64 — and
24
+ > adds a JS debugger, view-reload tracking and wait primitives for a UI that will not hold still.
25
+ > [**Read the full comparison →**](https://github.com/CitiesSkylinesModding/agents-plugins/blob/main/plugins/coherent-gameface/WHY.md)
26
+
21
27
  ## Requirements
22
28
 
23
29
  - **A Gameface application running** with its CDP debug endpoint reachable (default
package/dist/server.mjs CHANGED
@@ -32179,10 +32179,11 @@ async function gameWait(client, reloads, debug, options) {
32179
32179
  }
32180
32180
  }
32181
32181
  }
32182
- async function gameFill(client, selector, value, index = 0) {
32182
+ async function gameFill(client, options) {
32183
+ const { selector, value, index = 0, commit = false } = options;
32183
32184
  try {
32184
32185
  const res = await client.call("Runtime.evaluate", {
32185
- expression: callPageFn(fillFn, selector, value, index),
32186
+ expression: callPageFn(fillFn, selector, value, index, commit),
32186
32187
  returnByValue: true
32187
32188
  });
32188
32189
  if (res.exceptionDetails) {
@@ -32203,10 +32204,11 @@ async function gameFill(client, selector, value, index = 0) {
32203
32204
  return toErrorResult(error51);
32204
32205
  }
32205
32206
  }
32206
- async function gameType(client, selector, textToType, index = 0) {
32207
+ async function gameType(client, options) {
32208
+ const { selector, text: textToType, index = 0, commit = false } = options;
32207
32209
  try {
32208
32210
  const res = await client.call("Runtime.evaluate", {
32209
- expression: callPageFn(typeFn, selector, textToType, index),
32211
+ expression: callPageFn(typeFn, selector, textToType, index, commit),
32210
32212
  returnByValue: true
32211
32213
  });
32212
32214
  if (res.exceptionDetails) {
@@ -32530,7 +32532,7 @@ function waitCheckFn(sel, visible) {
32530
32532
  const rect = el.getBoundingClientRect();
32531
32533
  return rect.width > 0 && rect.height > 0;
32532
32534
  }
32533
- function fillFn(sel, value, index) {
32535
+ function fillFn(sel, value, index, commit) {
32534
32536
  const nodes = document.querySelectorAll(sel);
32535
32537
  if (nodes.length == 0) {
32536
32538
  return { found: false, count: 0 };
@@ -32549,6 +32551,7 @@ function fillFn(sel, value, index) {
32549
32551
  el.textContent = value;
32550
32552
  el.dispatchEvent(new Event("input", { bubbles: true }));
32551
32553
  el.dispatchEvent(new Event("change", { bubbles: true }));
32554
+ commitEdit();
32552
32555
  return { found: true, count, mode: "contenteditable", value: el.textContent ?? "" };
32553
32556
  }
32554
32557
  const field = el;
@@ -32561,9 +32564,22 @@ function fillFn(sel, value, index) {
32561
32564
  }
32562
32565
  el.dispatchEvent(new Event("input", { bubbles: true }));
32563
32566
  el.dispatchEvent(new Event("change", { bubbles: true }));
32567
+ commitEdit();
32564
32568
  return { found: true, count, mode: tag || "input", value: field.value };
32569
+ function commitEdit() {
32570
+ if (!commit) {
32571
+ return;
32572
+ }
32573
+ let event;
32574
+ try {
32575
+ event = new FocusEvent("focusout", { bubbles: true });
32576
+ } catch {
32577
+ event = new Event("focusout", { bubbles: true });
32578
+ }
32579
+ el.dispatchEvent(event);
32580
+ }
32565
32581
  }
32566
- function typeFn(sel, textToType, index) {
32582
+ function typeFn(sel, textToType, index, commit) {
32567
32583
  const nodes = document.querySelectorAll(sel);
32568
32584
  if (nodes.length == 0) {
32569
32585
  return { found: false, count: 0, typed: 0 };
@@ -32601,6 +32617,15 @@ function typeFn(sel, textToType, index) {
32601
32617
  typed++;
32602
32618
  }
32603
32619
  el.dispatchEvent(new Event("change", { bubbles: true }));
32620
+ if (commit) {
32621
+ let event;
32622
+ try {
32623
+ event = new FocusEvent("focusout", { bubbles: true });
32624
+ } catch {
32625
+ event = new Event("focusout", { bubbles: true });
32626
+ }
32627
+ el.dispatchEvent(event);
32628
+ }
32604
32629
  return { found: true, count, typed, value: current() };
32605
32630
  function current() {
32606
32631
  if (editable) {
@@ -32898,26 +32923,44 @@ async function main() {
32898
32923
  Set the value of an input, textarea, or contenteditable element and fire input/change so
32899
32924
  the UI framework reacts as if the user edited it.
32900
32925
  Best for setting a field in one shot; use game_type for keystrokes.
32926
+ If the value reverts (the field, or the state behind it, goes back to the old value), the
32927
+ field commits on blur: call again with commit true.
32928
+ A value that still reverts with commit true was rejected by the field; read it back rather
32929
+ than repeating the call.
32901
32930
  `,
32902
32931
  inputSchema: {
32903
32932
  selector: exports_external.string().describe(`CSS selector of the field to fill`),
32904
32933
  value: exports_external.string().describe(`Value to set`),
32905
- index: exports_external.number().int().min(0).optional().describe(`Which match to fill when several exist (default: 0)`)
32934
+ index: exports_external.number().int().min(0).optional().describe(`Which match to fill when several exist (default: 0)`),
32935
+ commit: exports_external.boolean().optional().describe(import_common_tags6.oneLine`
32936
+ Also dispatch a bubbling focusout after the value is set, so a field that commits on
32937
+ blur takes it. This call moves focus nowhere itself, but the page's own focusout
32938
+ handler may (default: false)
32939
+ `)
32906
32940
  }
32907
- }, ({ selector, value, index }) => gameFill(client, selector, value, index));
32941
+ }, ({ selector, value, index, commit }) => gameFill(client, { selector, value, index, commit }));
32908
32942
  server.registerTool("game_type", {
32909
32943
  title: `Type text into the Gameface UI`,
32910
32944
  description: import_common_tags6.oneLine`
32911
32945
  Type text into an element character by character, firing real KeyboardEvents plus keeping
32912
32946
  the value in sync.
32913
32947
  Use when handlers react to individual keystrokes; otherwise game_fill.
32948
+ If the value reverts (the field, or the state behind it, goes back to the old value), the
32949
+ field commits on blur: call again with commit true.
32950
+ A value that still reverts with commit true was rejected by the field; read it back rather
32951
+ than repeating the call, which would type the text a second time.
32914
32952
  `,
32915
32953
  inputSchema: {
32916
32954
  selector: exports_external.string().describe(`CSS selector of the field to type into`),
32917
32955
  text: exports_external.string().describe(`Text to type`),
32918
- index: exports_external.number().int().min(0).optional().describe(`Which match to type into when several exist (default: 0)`)
32956
+ index: exports_external.number().int().min(0).optional().describe(`Which match to type into when several exist (default: 0)`),
32957
+ commit: exports_external.boolean().optional().describe(import_common_tags6.oneLine`
32958
+ Also dispatch a bubbling focusout after the last keystroke, so a field that commits on
32959
+ blur takes the text. This call moves focus nowhere itself, but the page's own focusout
32960
+ handler may (default: false)
32961
+ `)
32919
32962
  }
32920
- }, ({ selector, text: text2, index }) => gameType(client, selector, text2, index));
32963
+ }, ({ selector, text: text2, index, commit }) => gameType(client, { selector, text: text2, index, commit }));
32921
32964
  server.registerTool("game_hover", {
32922
32965
  title: `Hover an element in the Gameface UI`,
32923
32966
  description: import_common_tags6.oneLine`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@csmodding/gameface-devtools-mcp",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "MCP server that drives a running Coherent Gameface (Cohtml) game UI over a direct Chrome DevTools Protocol connection: evaluate JS, screenshot, inspect and drive the DOM, capture the console, and set JS breakpoints.",
5
5
  "keywords": [
6
6
  "browser-automation",
@@ -42,6 +42,7 @@
42
42
  "@types/common-tags": "^1.8.4",
43
43
  "@types/node": "^24.13.3",
44
44
  "common-tags": "^1.8.2",
45
+ "happy-dom": "^20.12.2",
45
46
  "typescript": "^7.0.2",
46
47
  "zod": "^4.4.3"
47
48
  },