@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 +5 -4
- package/dist/server.mjs +101 -19
- package/package.json +2 -1
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
|
|
152
|
-
>
|
|
153
|
-
>
|
|
154
|
-
>
|
|
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
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
},
|