@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 +6 -0
- package/dist/server.mjs +53 -10
- package/package.json +2 -1
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,
|
|
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,
|
|
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.
|
|
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
|
},
|