@csmodding/gameface-devtools-mcp 1.1.1 → 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
@@ -148,10 +148,11 @@ Or in `.vscode/mcp.json` (note the `servers` key, not `mcpServers`):
148
148
 
149
149
  > [!IMPORTANT]
150
150
  > **Input is done via DOM events, not CDP `Input`.** Gameface accepts `Input.dispatchMouseEvent` /
151
- > `dispatchKeyEvent` but never delivers them to the UI. So `game_click`, `game_fill`, `game_type`,
152
- > `game_key`, and `game_hover` dispatch real DOM events in the page. These reach the UI's own JS
153
- > handlers, but not any input the game routes at the native/engine level (e.g. Escape-to-close is
154
- > often handled natively and will not respond to a dispatched key).
151
+ > `dispatchKeyEvent` but never delivers them to the UI — the mouse command measured on Cohtml 1.64
152
+ > and 2.2, the key and text ones on 2.2, with a 3.x Player untested. So `game_click`, `game_fill`,
153
+ > `game_type`, `game_key`, and `game_hover` dispatch real DOM events in the page. These reach the
154
+ > UI's own JS handlers, but not any input the game routes at the native/engine level (e.g.
155
+ > Escape-to-close is often handled natively and will not respond to a dispatched key).
155
156
 
156
157
  ### JS debugger tools
157
158
 
package/dist/server.mjs CHANGED
@@ -30622,12 +30622,16 @@ var SUPPORTED_PSEUDOS = new Set([
30622
30622
  ":before",
30623
30623
  ":after",
30624
30624
  "::before",
30625
- "::after"
30625
+ "::after",
30626
+ ":host"
30626
30627
  ]);
30628
+ var ANSWERED_NOT_REJECTED = new Set(["::slotted", "::part", "::selection"]);
30629
+ var ARGUMENTLESS_ANSWERED = new Set(["::selection"]);
30627
30630
  var SUPPORTED_SUMMARY = import_common_tags2.oneLine`
30628
30631
  type, class, id and attribute selectors, combinators, \`:first-child\`, \`:last-child\`,
30629
- \`:only-child\`, \`:nth-child()\`, \`:root\`, \`:hover\`, \`:focus\`, \`:active\`, \`::before\`
30630
- and \`::after\`
30632
+ \`:only-child\`, \`:nth-child()\`, \`:root\`, \`:hover\`, \`:focus\`, \`:active\`, \`::before\`,
30633
+ \`::after\`, and \`:host\` matched off the element alone rather than from a scoped query, which
30634
+ answers nothing
30631
30635
  `;
30632
30636
  var NTH_CHILD_ARGUMENT = /^\s*(?:[+-]?\d+|even|odd|[+-]?\d*n)\s*$/iu;
30633
30637
  var PSEUDO_TOKEN = /(?<colons>::?)(?<name>[a-z][a-z-]*)(?:\((?<argument>[^()]*)\))?/giu;
@@ -30723,6 +30727,9 @@ function maskLiterals(selector) {
30723
30727
  return LITERAL_SPANS.reduce((masked, span) => masked.replace(span, (literal3) => MASK_FILLER.repeat(literal3.length)), selector);
30724
30728
  }
30725
30729
  function isSupported(key, argument) {
30730
+ if (ANSWERED_NOT_REJECTED.has(key)) {
30731
+ return ARGUMENTLESS_ANSWERED.has(key) || argument != null && argument.trim() != "";
30732
+ }
30726
30733
  if (!SUPPORTED_PSEUDOS.has(key)) {
30727
30734
  return false;
30728
30735
  }
@@ -32179,10 +32186,11 @@ async function gameWait(client, reloads, debug, options) {
32179
32186
  }
32180
32187
  }
32181
32188
  }
32182
- async function gameFill(client, selector, value, index = 0) {
32189
+ async function gameFill(client, options) {
32190
+ const { selector, value, index = 0, commit = false } = options;
32183
32191
  try {
32184
32192
  const res = await client.call("Runtime.evaluate", {
32185
- expression: callPageFn(fillFn, selector, value, index),
32193
+ expression: callPageFn(fillFn, selector, value, index, commit),
32186
32194
  returnByValue: true
32187
32195
  });
32188
32196
  if (res.exceptionDetails) {
@@ -32203,10 +32211,11 @@ async function gameFill(client, selector, value, index = 0) {
32203
32211
  return toErrorResult(error51);
32204
32212
  }
32205
32213
  }
32206
- async function gameType(client, selector, textToType, index = 0) {
32214
+ async function gameType(client, options) {
32215
+ const { selector, text: textToType, index = 0, commit = false } = options;
32207
32216
  try {
32208
32217
  const res = await client.call("Runtime.evaluate", {
32209
- expression: callPageFn(typeFn, selector, textToType, index),
32218
+ expression: callPageFn(typeFn, selector, textToType, index, commit),
32210
32219
  returnByValue: true
32211
32220
  });
32212
32221
  if (res.exceptionDetails) {
@@ -32501,6 +32510,8 @@ function clickFn(sel, index) {
32501
32510
  cancelable: true,
32502
32511
  view: window,
32503
32512
  button: 0,
32513
+ screenX: cx,
32514
+ screenY: cy,
32504
32515
  clientX: cx,
32505
32516
  clientY: cy
32506
32517
  };
@@ -32530,7 +32541,7 @@ function waitCheckFn(sel, visible) {
32530
32541
  const rect = el.getBoundingClientRect();
32531
32542
  return rect.width > 0 && rect.height > 0;
32532
32543
  }
32533
- function fillFn(sel, value, index) {
32544
+ function fillFn(sel, value, index, commit) {
32534
32545
  const nodes = document.querySelectorAll(sel);
32535
32546
  if (nodes.length == 0) {
32536
32547
  return { found: false, count: 0 };
@@ -32549,6 +32560,7 @@ function fillFn(sel, value, index) {
32549
32560
  el.textContent = value;
32550
32561
  el.dispatchEvent(new Event("input", { bubbles: true }));
32551
32562
  el.dispatchEvent(new Event("change", { bubbles: true }));
32563
+ commitEdit();
32552
32564
  return { found: true, count, mode: "contenteditable", value: el.textContent ?? "" };
32553
32565
  }
32554
32566
  const field = el;
@@ -32561,9 +32573,22 @@ function fillFn(sel, value, index) {
32561
32573
  }
32562
32574
  el.dispatchEvent(new Event("input", { bubbles: true }));
32563
32575
  el.dispatchEvent(new Event("change", { bubbles: true }));
32576
+ commitEdit();
32564
32577
  return { found: true, count, mode: tag || "input", value: field.value };
32578
+ function commitEdit() {
32579
+ if (!commit) {
32580
+ return;
32581
+ }
32582
+ let event;
32583
+ try {
32584
+ event = new FocusEvent("focusout", { bubbles: true });
32585
+ } catch {
32586
+ event = new Event("focusout", { bubbles: true });
32587
+ }
32588
+ el.dispatchEvent(event);
32589
+ }
32565
32590
  }
32566
- function typeFn(sel, textToType, index) {
32591
+ function typeFn(sel, textToType, index, commit) {
32567
32592
  const nodes = document.querySelectorAll(sel);
32568
32593
  if (nodes.length == 0) {
32569
32594
  return { found: false, count: 0, typed: 0 };
@@ -32590,18 +32615,55 @@ function typeFn(sel, textToType, index) {
32590
32615
  key: ch,
32591
32616
  view: window
32592
32617
  };
32593
- try {
32594
- el.dispatchEvent(new KeyboardEvent("keydown", opts));
32595
- } catch {}
32618
+ sendKey("keydown", ch, opts);
32596
32619
  setValue(current() + ch);
32597
32620
  el.dispatchEvent(new Event("input", { bubbles: true }));
32598
- try {
32599
- el.dispatchEvent(new KeyboardEvent("keyup", opts));
32600
- } catch {}
32621
+ sendKey("keyup", ch, opts);
32601
32622
  typed++;
32602
32623
  }
32603
32624
  el.dispatchEvent(new Event("change", { bubbles: true }));
32625
+ if (commit) {
32626
+ let event;
32627
+ try {
32628
+ event = new FocusEvent("focusout", { bubbles: true });
32629
+ } catch {
32630
+ event = new Event("focusout", { bubbles: true });
32631
+ }
32632
+ el.dispatchEvent(event);
32633
+ }
32604
32634
  return { found: true, count, typed, value: current() };
32635
+ function sendKey(type, ch, init) {
32636
+ try {
32637
+ const ev = new KeyboardEvent(type, init);
32638
+ force(ev, "key", ch);
32639
+ const legacy = codeForChar(ch);
32640
+ if (legacy) {
32641
+ force(ev, "code", legacy.code);
32642
+ force(ev, "keyCode", legacy.keyCode);
32643
+ force(ev, "which", legacy.keyCode);
32644
+ }
32645
+ el.dispatchEvent(ev);
32646
+ } catch {}
32647
+ }
32648
+ function codeForChar(ch) {
32649
+ const SPACE_KEY_CODE = 32;
32650
+ if (ch == " ") {
32651
+ return { code: "Space", keyCode: SPACE_KEY_CODE };
32652
+ }
32653
+ if (/^[a-zA-Z]$/u.test(ch)) {
32654
+ const upper = ch.toUpperCase();
32655
+ return { code: `Key${upper}`, keyCode: upper.codePointAt(0) ?? 0 };
32656
+ }
32657
+ if (/^[0-9]$/u.test(ch)) {
32658
+ return { code: `Digit${ch}`, keyCode: ch.codePointAt(0) ?? 0 };
32659
+ }
32660
+ return;
32661
+ }
32662
+ function force(ev, prop, value) {
32663
+ try {
32664
+ Object.defineProperty(ev, prop, { get: () => value, configurable: true });
32665
+ } catch {}
32666
+ }
32605
32667
  function current() {
32606
32668
  if (editable) {
32607
32669
  return el.textContent ?? "";
@@ -32636,6 +32698,8 @@ function hoverFn(sel, index) {
32636
32698
  bubbles: true,
32637
32699
  cancelable: true,
32638
32700
  view: window,
32701
+ screenX: cx,
32702
+ screenY: cy,
32639
32703
  clientX: cx,
32640
32704
  clientY: cy
32641
32705
  };
@@ -32898,26 +32962,44 @@ async function main() {
32898
32962
  Set the value of an input, textarea, or contenteditable element and fire input/change so
32899
32963
  the UI framework reacts as if the user edited it.
32900
32964
  Best for setting a field in one shot; use game_type for keystrokes.
32965
+ If the value reverts (the field, or the state behind it, goes back to the old value), the
32966
+ field commits on blur: call again with commit true.
32967
+ A value that still reverts with commit true was rejected by the field; read it back rather
32968
+ than repeating the call.
32901
32969
  `,
32902
32970
  inputSchema: {
32903
32971
  selector: exports_external.string().describe(`CSS selector of the field to fill`),
32904
32972
  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)`)
32973
+ index: exports_external.number().int().min(0).optional().describe(`Which match to fill when several exist (default: 0)`),
32974
+ commit: exports_external.boolean().optional().describe(import_common_tags6.oneLine`
32975
+ Also dispatch a bubbling focusout after the value is set, so a field that commits on
32976
+ blur takes it. This call moves focus nowhere itself, but the page's own focusout
32977
+ handler may (default: false)
32978
+ `)
32906
32979
  }
32907
- }, ({ selector, value, index }) => gameFill(client, selector, value, index));
32980
+ }, ({ selector, value, index, commit }) => gameFill(client, { selector, value, index, commit }));
32908
32981
  server.registerTool("game_type", {
32909
32982
  title: `Type text into the Gameface UI`,
32910
32983
  description: import_common_tags6.oneLine`
32911
32984
  Type text into an element character by character, firing real KeyboardEvents plus keeping
32912
32985
  the value in sync.
32913
32986
  Use when handlers react to individual keystrokes; otherwise game_fill.
32987
+ If the value reverts (the field, or the state behind it, goes back to the old value), the
32988
+ field commits on blur: call again with commit true.
32989
+ A value that still reverts with commit true was rejected by the field; read it back rather
32990
+ than repeating the call, which would type the text a second time.
32914
32991
  `,
32915
32992
  inputSchema: {
32916
32993
  selector: exports_external.string().describe(`CSS selector of the field to type into`),
32917
32994
  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)`)
32995
+ index: exports_external.number().int().min(0).optional().describe(`Which match to type into when several exist (default: 0)`),
32996
+ commit: exports_external.boolean().optional().describe(import_common_tags6.oneLine`
32997
+ Also dispatch a bubbling focusout after the last keystroke, so a field that commits on
32998
+ blur takes the text. This call moves focus nowhere itself, but the page's own focusout
32999
+ handler may (default: false)
33000
+ `)
32919
33001
  }
32920
- }, ({ selector, text: text2, index }) => gameType(client, selector, text2, index));
33002
+ }, ({ selector, text: text2, index, commit }) => gameType(client, { selector, text: text2, index, commit }));
32921
33003
  server.registerTool("game_hover", {
32922
33004
  title: `Hover an element in the Gameface UI`,
32923
33005
  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.1",
3
+ "version": "1.2.1",
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
  },