@csmodding/gameface-devtools-mcp 1.0.1 → 1.1.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/dist/server.mjs +267 -97
- package/package.json +1 -1
package/dist/server.mjs
CHANGED
|
@@ -30319,7 +30319,7 @@ class StdioServerTransport {
|
|
|
30319
30319
|
}
|
|
30320
30320
|
|
|
30321
30321
|
// src/server.ts
|
|
30322
|
-
var
|
|
30322
|
+
var import_common_tags6 = __toESM(require_lib(), 1);
|
|
30323
30323
|
|
|
30324
30324
|
// src/cdp.ts
|
|
30325
30325
|
var import_common_tags = __toESM(require_lib(), 1);
|
|
@@ -30603,7 +30603,143 @@ function num(value, fallback) {
|
|
|
30603
30603
|
}
|
|
30604
30604
|
|
|
30605
30605
|
// src/console.ts
|
|
30606
|
+
var import_common_tags3 = __toESM(require_lib(), 1);
|
|
30607
|
+
|
|
30608
|
+
// src/selectors.ts
|
|
30606
30609
|
var import_common_tags2 = __toESM(require_lib(), 1);
|
|
30610
|
+
var INVALID_SELECTOR_MARKER = "Invalid CSS selector";
|
|
30611
|
+
var SELECTOR_PREFIX = `${INVALID_SELECTOR_MARKER} (`;
|
|
30612
|
+
var SELECTOR_TAIL = /\) in \w+!/gu;
|
|
30613
|
+
var SUPPORTED_PSEUDOS = new Set([
|
|
30614
|
+
":first-child",
|
|
30615
|
+
":last-child",
|
|
30616
|
+
":only-child",
|
|
30617
|
+
":nth-child",
|
|
30618
|
+
":root",
|
|
30619
|
+
":hover",
|
|
30620
|
+
":focus",
|
|
30621
|
+
":active",
|
|
30622
|
+
":before",
|
|
30623
|
+
":after",
|
|
30624
|
+
"::before",
|
|
30625
|
+
"::after"
|
|
30626
|
+
]);
|
|
30627
|
+
var SUPPORTED_SUMMARY = import_common_tags2.oneLine`
|
|
30628
|
+
type, class, id and attribute selectors, combinators, \`:first-child\`, \`:last-child\`,
|
|
30629
|
+
\`:only-child\`, \`:nth-child()\`, \`:root\`, \`:hover\`, \`:focus\`, \`:active\`, \`::before\`
|
|
30630
|
+
and \`::after\`
|
|
30631
|
+
`;
|
|
30632
|
+
var NTH_CHILD_ARGUMENT = /^\s*(?:[+-]?\d+|even|odd|[+-]?\d*n)\s*$/iu;
|
|
30633
|
+
var PSEUDO_TOKEN = /(?<colons>::?)(?<name>[a-z][a-z-]*)(?:\((?<argument>[^()]*)\))?/giu;
|
|
30634
|
+
var LITERAL_SPANS = [/\\./gu, /"[^"]*"|'[^']*'/gu, /\[[^\])]*\]?/gu];
|
|
30635
|
+
var MASK_FILLER = "#";
|
|
30636
|
+
var SCAN_PATTERN = scanPattern();
|
|
30637
|
+
var BRANCH_PER_QUERY = `run one query per branch of the list and merge the results.`;
|
|
30638
|
+
var DISABLED_HINT = `${scanPattern("`el.disabled`")}.`;
|
|
30639
|
+
var HINTS = new Map([
|
|
30640
|
+
[":not", `${SCAN_PATTERN}.`],
|
|
30641
|
+
[":has", `${scanPattern("what the element contains")}.`],
|
|
30642
|
+
[":is", BRANCH_PER_QUERY],
|
|
30643
|
+
[":where", BRANCH_PER_QUERY],
|
|
30644
|
+
[":first-of-type", ofTypeHint(":first-child")],
|
|
30645
|
+
[":last-of-type", ofTypeHint(":last-child")],
|
|
30646
|
+
[":only-of-type", ofTypeHint(":only-child")],
|
|
30647
|
+
[":nth-of-type", ofTypeHint(":nth-child()")],
|
|
30648
|
+
[
|
|
30649
|
+
":nth-child",
|
|
30650
|
+
import_common_tags2.oneLine`
|
|
30651
|
+
the engine takes an integer, \`even\`, \`odd\`, or a bare \`an\` step here, and throws on an
|
|
30652
|
+
\`an+b\` offset.
|
|
30653
|
+
`
|
|
30654
|
+
],
|
|
30655
|
+
[
|
|
30656
|
+
":nth-last-child",
|
|
30657
|
+
`\`:nth-child()\` counts from the start only, so ${scanPattern("a position from the end")}.`
|
|
30658
|
+
],
|
|
30659
|
+
[":empty", `${scanPattern("`el.children.length` and `el.textContent`")}.`],
|
|
30660
|
+
[":checked", `${scanPattern("`el.checked`")}.`],
|
|
30661
|
+
[":disabled", DISABLED_HINT],
|
|
30662
|
+
[":enabled", DISABLED_HINT]
|
|
30663
|
+
]);
|
|
30664
|
+
var GENERIC_HINT = import_common_tags2.oneLine`
|
|
30665
|
+
express the condition in JS (${SCAN_PATTERN}) or drop it from the selector.
|
|
30666
|
+
`;
|
|
30667
|
+
var DIAGNOSIS_LEAD = `Cohtml's JS query APIs rejected this selector.`;
|
|
30668
|
+
var SYNTAX_SLIP = `a syntax slip, an unclosed bracket or quote`;
|
|
30669
|
+
var SYNTAX_SLIP_EXIT = `If that is not the fault, check the selector for ${SYNTAX_SLIP}.`;
|
|
30670
|
+
var GENERIC_DIAGNOSIS = import_common_tags2.oneLine`
|
|
30671
|
+
${DIAGNOSIS_LEAD} It names no construct known to be unsupported, so check it for ${SYNTAX_SLIP}.
|
|
30672
|
+
Verified to work here: ${SUPPORTED_SUMMARY}; a pseudo-class outside that set may still throw.
|
|
30673
|
+
`;
|
|
30674
|
+
function isInvalidSelectorError(engineError) {
|
|
30675
|
+
return engineError.includes(INVALID_SELECTOR_MARKER);
|
|
30676
|
+
}
|
|
30677
|
+
function diagnoseSelectorError(engineError, selector) {
|
|
30678
|
+
if (!isInvalidSelectorError(engineError)) {
|
|
30679
|
+
return engineError;
|
|
30680
|
+
}
|
|
30681
|
+
const suspects = findSuspects(selector ?? selectorFromError(engineError));
|
|
30682
|
+
if (suspects.length == 0) {
|
|
30683
|
+
return `${engineError}
|
|
30684
|
+
|
|
30685
|
+
${GENERIC_DIAGNOSIS}`;
|
|
30686
|
+
}
|
|
30687
|
+
const sentences = suspects.map(({ token, hint }) => `\`${token}\` falls outside the set this engine answers: ${hint}`);
|
|
30688
|
+
return `${engineError}
|
|
30689
|
+
|
|
30690
|
+
${DIAGNOSIS_LEAD} ${sentences.join(" ")} ${SYNTAX_SLIP_EXIT}`;
|
|
30691
|
+
}
|
|
30692
|
+
function selectorFromError(engineError) {
|
|
30693
|
+
const start = engineError.indexOf(SELECTOR_PREFIX);
|
|
30694
|
+
if (start == -1) {
|
|
30695
|
+
return "";
|
|
30696
|
+
}
|
|
30697
|
+
const afterPrefix = engineError.slice(start + SELECTOR_PREFIX.length);
|
|
30698
|
+
const tails = [...afterPrefix.matchAll(SELECTOR_TAIL)];
|
|
30699
|
+
return tails.length == 1 ? afterPrefix.slice(0, tails[0]?.index) : "";
|
|
30700
|
+
}
|
|
30701
|
+
function findSuspects(selector) {
|
|
30702
|
+
const suspects = [];
|
|
30703
|
+
const seen = new Set;
|
|
30704
|
+
const scanner = new RegExp(PSEUDO_TOKEN.source, PSEUDO_TOKEN.flags);
|
|
30705
|
+
const scannable = maskLiterals(selector);
|
|
30706
|
+
let match = scanner.exec(scannable);
|
|
30707
|
+
while (match != null) {
|
|
30708
|
+
const { colons = "", name = "", argument } = match.groups ?? {};
|
|
30709
|
+
const key = `${colons}${name}`.toLowerCase();
|
|
30710
|
+
if (!seen.has(key) && !isSupported(key, argument)) {
|
|
30711
|
+
seen.add(key);
|
|
30712
|
+
suspects.push({
|
|
30713
|
+
token: selector.slice(match.index, match.index + match[0].length),
|
|
30714
|
+
hint: HINTS.get(key) ?? GENERIC_HINT
|
|
30715
|
+
});
|
|
30716
|
+
}
|
|
30717
|
+
scanner.lastIndex = match.index + colons.length + name.length;
|
|
30718
|
+
match = scanner.exec(scannable);
|
|
30719
|
+
}
|
|
30720
|
+
return suspects;
|
|
30721
|
+
}
|
|
30722
|
+
function maskLiterals(selector) {
|
|
30723
|
+
return LITERAL_SPANS.reduce((masked, span) => masked.replace(span, (literal3) => MASK_FILLER.repeat(literal3.length)), selector);
|
|
30724
|
+
}
|
|
30725
|
+
function isSupported(key, argument) {
|
|
30726
|
+
if (!SUPPORTED_PSEUDOS.has(key)) {
|
|
30727
|
+
return false;
|
|
30728
|
+
}
|
|
30729
|
+
return key != ":nth-child" || argument == null || NTH_CHILD_ARGUMENT.test(argument);
|
|
30730
|
+
}
|
|
30731
|
+
function scanPattern(condition) {
|
|
30732
|
+
return import_common_tags2.oneLine`
|
|
30733
|
+
filter \`querySelectorAll\` results in \`game_eval\`${condition ? ` on ${condition}` : ""},
|
|
30734
|
+
tag the element you want with a data attribute, then query that attribute
|
|
30735
|
+
`;
|
|
30736
|
+
}
|
|
30737
|
+
function ofTypeHint(equivalent) {
|
|
30738
|
+
return import_common_tags2.oneLine`
|
|
30739
|
+
use \`${equivalent}\` where the parent holds only that element type,
|
|
30740
|
+
otherwise ${SCAN_PATTERN}.
|
|
30741
|
+
`;
|
|
30742
|
+
}
|
|
30607
30743
|
|
|
30608
30744
|
// src/shared.ts
|
|
30609
30745
|
function text(value) {
|
|
@@ -30635,6 +30771,9 @@ function formatException(details) {
|
|
|
30635
30771
|
const desc = exception?.description ?? exception?.value ?? details?.text ?? `unknown error`;
|
|
30636
30772
|
return typeof desc == "string" ? desc : JSON.stringify(desc);
|
|
30637
30773
|
}
|
|
30774
|
+
function explainException(details, selector) {
|
|
30775
|
+
return diagnoseSelectorError(formatException(details), selector);
|
|
30776
|
+
}
|
|
30638
30777
|
function valToStr(value) {
|
|
30639
30778
|
if (typeof value == "string") {
|
|
30640
30779
|
return value;
|
|
@@ -30791,7 +30930,7 @@ async function gameConsole(client, buffer, options) {
|
|
|
30791
30930
|
}
|
|
30792
30931
|
const lines = buffer.read(options);
|
|
30793
30932
|
if (lines.length == 0) {
|
|
30794
|
-
return text(
|
|
30933
|
+
return text(import_common_tags3.oneLine`
|
|
30795
30934
|
No console entries captured yet.
|
|
30796
30935
|
Capture begins once the server connects to the application;
|
|
30797
30936
|
trigger some UI activity (or a game_eval console.log) and retry.
|
|
@@ -31113,7 +31252,7 @@ function unknownValue() {
|
|
|
31113
31252
|
}
|
|
31114
31253
|
|
|
31115
31254
|
// src/debugger.ts
|
|
31116
|
-
var
|
|
31255
|
+
var import_common_tags4 = __toESM(require_lib(), 1);
|
|
31117
31256
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
31118
31257
|
var POLL_INTERVAL_MS = 50;
|
|
31119
31258
|
var PAUSE_WAIT_MS = 3000;
|
|
@@ -31123,32 +31262,32 @@ var MAX_SCOPE_VARIABLES = 50;
|
|
|
31123
31262
|
var MAX_SEARCH_MATCHES = 10;
|
|
31124
31263
|
var SEARCH_SNIPPET_MARGIN = 40;
|
|
31125
31264
|
var DEFAULT_SOURCE_MAX_CHARS = 4000;
|
|
31126
|
-
var EMPTY_SCRIPT_MAP_NOTE =
|
|
31265
|
+
var EMPTY_SCRIPT_MAP_NOTE = import_common_tags4.oneLine`
|
|
31127
31266
|
No scripts are parsed on this connection: a view reload re-parses every one of them.
|
|
31128
31267
|
Take the reload count from game_status, game_eval of location.reload(), then game_wait with
|
|
31129
31268
|
reload true and that count as sinceReloads, then call this again.
|
|
31130
31269
|
`;
|
|
31131
|
-
var NO_MATCH_NOTE =
|
|
31270
|
+
var NO_MATCH_NOTE = import_common_tags4.oneLine`
|
|
31132
31271
|
Nothing matched. The search is case-sensitive and literal (no regex), and it only reaches scripts
|
|
31133
31272
|
parsed on this connection: game_debug_scripts lists them.
|
|
31134
31273
|
`;
|
|
31135
|
-
var FILTER_MISS_NOTE =
|
|
31274
|
+
var FILTER_MISS_NOTE = import_common_tags4.oneLine`
|
|
31136
31275
|
No parsed script's url contains that urlContains, so the query never ran against anything.
|
|
31137
31276
|
game_debug_scripts lists the urls; the url filter is case-insensitive, so case is not the cause.
|
|
31138
31277
|
`;
|
|
31139
|
-
var FILTER_MISS_LIST_NOTE =
|
|
31278
|
+
var FILTER_MISS_LIST_NOTE = import_common_tags4.oneLine`
|
|
31140
31279
|
No parsed script's url contains that filter, though others are parsed: call this without one to
|
|
31141
31280
|
see every url. Matching is case-insensitive, so case is not the cause.
|
|
31142
31281
|
`;
|
|
31143
|
-
var UNREADABLE_NOTE =
|
|
31282
|
+
var UNREADABLE_NOTE = import_common_tags4.oneLine`
|
|
31144
31283
|
The engine has no source for any of the scripts matched, so the query never ran: their ids are
|
|
31145
31284
|
stale, which a view reload clears by re-parsing everything under fresh ones.
|
|
31146
31285
|
`;
|
|
31147
|
-
var SEARCH_HINT =
|
|
31286
|
+
var SEARCH_HINT = import_common_tags4.oneLine`
|
|
31148
31287
|
Pass a match's line and column to game_debug_set_breakpoint to break there; both are 1-based, as
|
|
31149
31288
|
everywhere in these tools, and its urlContains takes the match's url as printed.
|
|
31150
31289
|
`;
|
|
31151
|
-
var SINGLE_LINE_HINT =
|
|
31290
|
+
var SINGLE_LINE_HINT = import_common_tags4.oneLine`
|
|
31152
31291
|
That script is one line, as a minified bundle is, so the whole module sits on the line the
|
|
31153
31292
|
resolved location names and the breakpoint bound to the first breakable position at or after the
|
|
31154
31293
|
one you asked for: read the column above to see where it landed.
|
|
@@ -31274,7 +31413,7 @@ class DebuggerSession {
|
|
|
31274
31413
|
const cap = 400;
|
|
31275
31414
|
const from = lineStart && lineStart > first ? lineStart : first;
|
|
31276
31415
|
if (from > last) {
|
|
31277
|
-
return errorText(
|
|
31416
|
+
return errorText(import_common_tags4.oneLine`
|
|
31278
31417
|
Script ${scriptId} spans lines ${first}-${last}, so there is nothing at line ${from}.
|
|
31279
31418
|
`);
|
|
31280
31419
|
}
|
|
@@ -31292,7 +31431,7 @@ class DebuggerSession {
|
|
|
31292
31431
|
notes.push(`showing lines ${from}-${shownTo} of ${last}`);
|
|
31293
31432
|
}
|
|
31294
31433
|
if (clipped) {
|
|
31295
|
-
notes.push(
|
|
31434
|
+
notes.push(import_common_tags4.oneLine`
|
|
31296
31435
|
clipped to ${budget} of ${rendered.length} characters: raise maxChars for more, or reach
|
|
31297
31436
|
a position inside a minified line with game_debug_search_source
|
|
31298
31437
|
`);
|
|
@@ -31336,10 +31475,10 @@ class DebuggerSession {
|
|
|
31336
31475
|
condition,
|
|
31337
31476
|
resolvedLocations: resolved,
|
|
31338
31477
|
pending: locations.length == 0,
|
|
31339
|
-
note: locations.length == 0 ?
|
|
31478
|
+
note: locations.length == 0 ? import_common_tags4.oneLine`
|
|
31340
31479
|
Pending: no matching script/line loaded yet, or the line has no code.
|
|
31341
31480
|
It will bind when the script loads.
|
|
31342
|
-
` :
|
|
31481
|
+
` : import_common_tags4.oneLine`
|
|
31343
31482
|
Hitting this breakpoint FREEZES the UI until you resume
|
|
31344
31483
|
(game_debug_step resume).
|
|
31345
31484
|
`,
|
|
@@ -31402,7 +31541,7 @@ class DebuggerSession {
|
|
|
31402
31541
|
if (this.paused) {
|
|
31403
31542
|
const frame = this.paused.callFrames[frameIndex];
|
|
31404
31543
|
if (!frame) {
|
|
31405
|
-
return errorText(
|
|
31544
|
+
return errorText(import_common_tags4.oneLine`
|
|
31406
31545
|
No call frame at index ${frameIndex}
|
|
31407
31546
|
(paused stack has ${this.paused.callFrames.length}).
|
|
31408
31547
|
`);
|
|
@@ -31414,7 +31553,7 @@ class DebuggerSession {
|
|
|
31414
31553
|
silent: true
|
|
31415
31554
|
});
|
|
31416
31555
|
if (res2.exceptionDetails) {
|
|
31417
|
-
return errorText(`Eval threw: ${
|
|
31556
|
+
return errorText(`Eval threw: ${explainException(res2.exceptionDetails)}`);
|
|
31418
31557
|
}
|
|
31419
31558
|
const value2 = describeRemoteObject(res2.result);
|
|
31420
31559
|
return text(typeof value2 == "string" ? value2 : valToStr(value2));
|
|
@@ -31424,7 +31563,7 @@ class DebuggerSession {
|
|
|
31424
31563
|
returnByValue: true
|
|
31425
31564
|
});
|
|
31426
31565
|
if (res.exceptionDetails) {
|
|
31427
|
-
return errorText(`Eval threw: ${
|
|
31566
|
+
return errorText(`Eval threw: ${explainException(res.exceptionDetails)}`);
|
|
31428
31567
|
}
|
|
31429
31568
|
const value = describeRemoteObject(res.result);
|
|
31430
31569
|
return text(typeof value == "string" ? value : valToStr(value));
|
|
@@ -31451,7 +31590,7 @@ class DebuggerSession {
|
|
|
31451
31590
|
await this.client.call("Debugger.resume");
|
|
31452
31591
|
await this.waitUntil(() => !this.paused, RESUME_WAIT_MS);
|
|
31453
31592
|
if (this.paused) {
|
|
31454
|
-
return errorText(
|
|
31593
|
+
return errorText(import_common_tags4.oneLine`
|
|
31455
31594
|
Resume sent, but the UI is paused again at ${this.topLocation()}
|
|
31456
31595
|
(reason: ${this.paused.reason}), so it is still frozen.
|
|
31457
31596
|
Whatever armed that pause sits on the resumed path, so clear it before resuming again:
|
|
@@ -31665,7 +31804,7 @@ function snippetAt(source, index, length) {
|
|
|
31665
31804
|
}
|
|
31666
31805
|
|
|
31667
31806
|
// src/tools.ts
|
|
31668
|
-
var
|
|
31807
|
+
var import_common_tags5 = __toESM(require_lib(), 1);
|
|
31669
31808
|
import { setTimeout as sleep2 } from "node:timers/promises";
|
|
31670
31809
|
var POLL_INTERVAL_MS2 = 150;
|
|
31671
31810
|
var MAX_WAIT_MS = 60000;
|
|
@@ -31713,7 +31852,7 @@ async function gameStatus(client, reloads) {
|
|
|
31713
31852
|
reachable: false,
|
|
31714
31853
|
endpoint: `http://${host}:${port}`,
|
|
31715
31854
|
error: error51 instanceof Error ? error51.message : String(error51),
|
|
31716
|
-
hint:
|
|
31855
|
+
hint: import_common_tags5.oneLine`
|
|
31717
31856
|
Launch the Gameface application with its CDP debug port open, then retry.
|
|
31718
31857
|
Override host/port via GAMEFACE_HOST / GAMEFACE_PORT.
|
|
31719
31858
|
`
|
|
@@ -31728,7 +31867,7 @@ async function gameEval(client, expression, awaitPromise = false) {
|
|
|
31728
31867
|
awaitPromise
|
|
31729
31868
|
});
|
|
31730
31869
|
if (res.exceptionDetails) {
|
|
31731
|
-
return errorText(`Evaluation threw: ${
|
|
31870
|
+
return errorText(`Evaluation threw: ${explainException(res.exceptionDetails)}`);
|
|
31732
31871
|
}
|
|
31733
31872
|
const value = describeRemoteObject(res.result);
|
|
31734
31873
|
return text(typeof value == "string" ? value : JSON.stringify(value, null, 2));
|
|
@@ -31742,7 +31881,7 @@ async function gameScreenshot(client, debug, options = {}) {
|
|
|
31742
31881
|
await client.connection();
|
|
31743
31882
|
const { pause } = debug;
|
|
31744
31883
|
if (pause) {
|
|
31745
|
-
return errorText(
|
|
31884
|
+
return errorText(import_common_tags5.oneLine`
|
|
31746
31885
|
game_screenshot cannot capture while the JS debugger holds the UI paused
|
|
31747
31886
|
(${pause.reason}, at ${pause.location}): the capture needs the frame loop the pause has
|
|
31748
31887
|
frozen, so it would hang until the call times out.
|
|
@@ -31757,26 +31896,14 @@ async function gameScreenshot(client, debug, options = {}) {
|
|
|
31757
31896
|
if (format == "jpeg") {
|
|
31758
31897
|
params.quality = quality ?? DEFAULT_JPEG_QUALITY;
|
|
31759
31898
|
}
|
|
31760
|
-
let
|
|
31899
|
+
let clipCaption;
|
|
31761
31900
|
if (selector) {
|
|
31762
|
-
const
|
|
31763
|
-
|
|
31764
|
-
|
|
31765
|
-
});
|
|
31766
|
-
const rect = rectRes.result.value;
|
|
31767
|
-
if (!rect?.found) {
|
|
31768
|
-
return errorText(import_common_tags4.oneLine`
|
|
31769
|
-
No element matched ${JSON.stringify(selector)} for game_screenshot at index ${index}
|
|
31770
|
-
(matches found: ${rect?.count ?? 0}).
|
|
31771
|
-
`);
|
|
31772
|
-
}
|
|
31773
|
-
if (!(rect.width > 0 && rect.height > 0)) {
|
|
31774
|
-
return errorText(`Element ${JSON.stringify(selector)} has a zero-size box; nothing to capture.`);
|
|
31901
|
+
const clip = await resolveClip(client, selector, index);
|
|
31902
|
+
if (!clip.ok) {
|
|
31903
|
+
return errorText(clip.error);
|
|
31775
31904
|
}
|
|
31776
|
-
params.clip =
|
|
31777
|
-
|
|
31778
|
-
Clipped to ${JSON.stringify(selector)} [index ${index}]. Matches: ${rect.count}.
|
|
31779
|
-
`;
|
|
31905
|
+
params.clip = clip.rect;
|
|
31906
|
+
clipCaption = clip.caption;
|
|
31780
31907
|
}
|
|
31781
31908
|
const res = await client.call("Page.captureScreenshot", params);
|
|
31782
31909
|
if (!res?.data) {
|
|
@@ -31787,11 +31914,46 @@ async function gameScreenshot(client, debug, options = {}) {
|
|
|
31787
31914
|
data: res.data,
|
|
31788
31915
|
mimeType: format == "jpeg" ? "image/jpeg" : "image/png"
|
|
31789
31916
|
};
|
|
31790
|
-
return { content:
|
|
31917
|
+
return { content: clipCaption ? [{ type: "text", text: clipCaption }, image] : [image] };
|
|
31791
31918
|
} catch (error51) {
|
|
31792
31919
|
return toErrorResult(error51);
|
|
31793
31920
|
}
|
|
31794
31921
|
}
|
|
31922
|
+
async function resolveClip(client, selector, index) {
|
|
31923
|
+
const rectRes = await client.call("Runtime.evaluate", {
|
|
31924
|
+
expression: callPageFn(rectFn, selector, index),
|
|
31925
|
+
returnByValue: true
|
|
31926
|
+
});
|
|
31927
|
+
if (rectRes.exceptionDetails) {
|
|
31928
|
+
return {
|
|
31929
|
+
ok: false,
|
|
31930
|
+
error: `Screenshot clip failed: ${explainException(rectRes.exceptionDetails, selector)}`
|
|
31931
|
+
};
|
|
31932
|
+
}
|
|
31933
|
+
const rect = rectRes.result.value;
|
|
31934
|
+
if (!rect?.found) {
|
|
31935
|
+
return {
|
|
31936
|
+
ok: false,
|
|
31937
|
+
error: import_common_tags5.oneLine`
|
|
31938
|
+
No element matched ${JSON.stringify(selector)} for game_screenshot at index ${index}
|
|
31939
|
+
(matches found: ${rect?.count ?? 0}).
|
|
31940
|
+
`
|
|
31941
|
+
};
|
|
31942
|
+
}
|
|
31943
|
+
if (!(rect.width > 0 && rect.height > 0)) {
|
|
31944
|
+
return {
|
|
31945
|
+
ok: false,
|
|
31946
|
+
error: `Element ${JSON.stringify(selector)} has a zero-size box; nothing to capture.`
|
|
31947
|
+
};
|
|
31948
|
+
}
|
|
31949
|
+
return {
|
|
31950
|
+
ok: true,
|
|
31951
|
+
rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height, scale: 1 },
|
|
31952
|
+
caption: import_common_tags5.oneLine`
|
|
31953
|
+
Clipped to ${JSON.stringify(selector)} [index ${index}]. Matches: ${rect.count}.
|
|
31954
|
+
`
|
|
31955
|
+
};
|
|
31956
|
+
}
|
|
31795
31957
|
async function gameDom(client, selector, all = false, maxHtml = 4000) {
|
|
31796
31958
|
try {
|
|
31797
31959
|
const res = await client.call("Runtime.evaluate", {
|
|
@@ -31799,7 +31961,7 @@ async function gameDom(client, selector, all = false, maxHtml = 4000) {
|
|
|
31799
31961
|
returnByValue: true
|
|
31800
31962
|
});
|
|
31801
31963
|
if (res.exceptionDetails) {
|
|
31802
|
-
return errorText(`DOM query failed: ${
|
|
31964
|
+
return errorText(`DOM query failed: ${explainException(res.exceptionDetails, selector)}`);
|
|
31803
31965
|
}
|
|
31804
31966
|
const value = res.result.value;
|
|
31805
31967
|
if (!value || value.count == 0) {
|
|
@@ -31834,7 +31996,7 @@ async function gameQuery(client, options) {
|
|
|
31834
31996
|
returnByValue: true
|
|
31835
31997
|
});
|
|
31836
31998
|
if (res.exceptionDetails) {
|
|
31837
|
-
return errorText(`Query failed: ${
|
|
31999
|
+
return errorText(`Query failed: ${explainException(res.exceptionDetails, args.sel)}`);
|
|
31838
32000
|
}
|
|
31839
32001
|
const value = res.result.value;
|
|
31840
32002
|
if (!value) {
|
|
@@ -31863,13 +32025,13 @@ function queryRejection(options, mode) {
|
|
|
31863
32025
|
return `game_query: selector is blank; pass a real one.`;
|
|
31864
32026
|
}
|
|
31865
32027
|
if (mode == "equals" && needle != null && needle.trim() != needle) {
|
|
31866
|
-
return
|
|
32028
|
+
return import_common_tags5.oneLine`
|
|
31867
32029
|
game_query: equals compares against trimmed text, so trim yours; where that empties it, pass
|
|
31868
32030
|
'' with a selector to find elements carrying no text.
|
|
31869
32031
|
`;
|
|
31870
32032
|
}
|
|
31871
32033
|
if (needle == "" && mode != "equals") {
|
|
31872
|
-
return
|
|
32034
|
+
return import_common_tags5.oneLine`
|
|
31873
32035
|
game_query: empty text matches every element under ${mode}; pass match: 'equals' with a
|
|
31874
32036
|
selector to find elements carrying no text.
|
|
31875
32037
|
`;
|
|
@@ -31884,7 +32046,7 @@ function pausedNote(debug) {
|
|
|
31884
32046
|
if (!pause) {
|
|
31885
32047
|
return "";
|
|
31886
32048
|
}
|
|
31887
|
-
return
|
|
32049
|
+
return import_common_tags5.oneLine`
|
|
31888
32050
|
The UI is paused in the JS debugger (${pause.reason}, at ${pause.location}): nothing in it can
|
|
31889
32051
|
change until you resume with game_debug_step action=resume, so resume and retry rather than
|
|
31890
32052
|
reading this as an unreachable condition.
|
|
@@ -31897,16 +32059,16 @@ async function gameClick(client, selector, index = 0) {
|
|
|
31897
32059
|
returnByValue: true
|
|
31898
32060
|
});
|
|
31899
32061
|
if (res.exceptionDetails) {
|
|
31900
|
-
return errorText(`Click failed: ${
|
|
32062
|
+
return errorText(`Click failed: ${explainException(res.exceptionDetails, selector)}`);
|
|
31901
32063
|
}
|
|
31902
32064
|
const info = res.result.value;
|
|
31903
32065
|
if (!info?.found) {
|
|
31904
|
-
return errorText(
|
|
32066
|
+
return errorText(import_common_tags5.oneLine`
|
|
31905
32067
|
No element to click for selector ${JSON.stringify(selector)} at index ${index}
|
|
31906
32068
|
(matches found: ${info?.count ?? 0}).
|
|
31907
32069
|
`);
|
|
31908
32070
|
}
|
|
31909
|
-
return text(
|
|
32071
|
+
return text(import_common_tags5.oneLine`
|
|
31910
32072
|
Clicked ${JSON.stringify(selector)} [index ${index}] at
|
|
31911
32073
|
(${info.x.toFixed(0)}, ${info.y.toFixed(0)}).
|
|
31912
32074
|
Dispatched: ${info.fired.join(", ")}. Matches: ${info.count}.
|
|
@@ -31957,7 +32119,7 @@ async function gameWait(client, reloads, debug, options) {
|
|
|
31957
32119
|
const baseline = sinceReloads ?? reloads.count;
|
|
31958
32120
|
while (reloads.count <= baseline) {
|
|
31959
32121
|
if (Date.now() >= deadline) {
|
|
31960
|
-
return timedOut(
|
|
32122
|
+
return timedOut(import_common_tags5.oneLine`
|
|
31961
32123
|
Timed out after ${budget}ms waiting for a view reload
|
|
31962
32124
|
(reload count still ${reloads.count}, baseline ${baseline}).
|
|
31963
32125
|
`);
|
|
@@ -31969,7 +32131,7 @@ async function gameWait(client, reloads, debug, options) {
|
|
|
31969
32131
|
let quietSince = Date.now();
|
|
31970
32132
|
while (Date.now() - quietSince < quiescentMs) {
|
|
31971
32133
|
if (Date.now() >= deadline) {
|
|
31972
|
-
return timedOut(
|
|
32134
|
+
return timedOut(import_common_tags5.oneLine`
|
|
31973
32135
|
Timed out after ${budget}ms: reload observed, but context swaps kept arriving within
|
|
31974
32136
|
the ${quiescentMs}ms quiescence window.
|
|
31975
32137
|
`);
|
|
@@ -31993,6 +32155,13 @@ async function gameWait(client, reloads, debug, options) {
|
|
|
31993
32155
|
});
|
|
31994
32156
|
if (res.exceptionDetails) {
|
|
31995
32157
|
lastError = formatException(res.exceptionDetails);
|
|
32158
|
+
if (selector && isInvalidSelectorError(lastError)) {
|
|
32159
|
+
const preamble = import_common_tags5.oneLine`
|
|
32160
|
+
game_wait stopped after ${Date.now() - start}ms without waiting out the ${budget}ms
|
|
32161
|
+
budget:
|
|
32162
|
+
`;
|
|
32163
|
+
return errorText(`${preamble} ${diagnoseSelectorError(lastError, selector)}`);
|
|
32164
|
+
}
|
|
31996
32165
|
} else if (res.result.value) {
|
|
31997
32166
|
const what = selector ? `selector ${JSON.stringify(selector)}` : "predicate";
|
|
31998
32167
|
const reloadNote = reload ? ` Reload count: ${reloads.count}.` : "";
|
|
@@ -32002,7 +32171,8 @@ async function gameWait(client, reloads, debug, options) {
|
|
|
32002
32171
|
}
|
|
32003
32172
|
if (Date.now() >= deadline) {
|
|
32004
32173
|
const what = selector ? `selector ${JSON.stringify(selector)}` : "predicate";
|
|
32005
|
-
const
|
|
32174
|
+
const lastNote = lastError && diagnoseSelectorError(lastError);
|
|
32175
|
+
const errorNote = lastNote ? ` Last predicate error: ${lastNote}` : "";
|
|
32006
32176
|
return timedOut(`Timed out after ${budget}ms waiting for ${what}.${errorNote}`);
|
|
32007
32177
|
}
|
|
32008
32178
|
await sleep2(POLL_INTERVAL_MS2);
|
|
@@ -32016,16 +32186,16 @@ async function gameFill(client, selector, value, index = 0) {
|
|
|
32016
32186
|
returnByValue: true
|
|
32017
32187
|
});
|
|
32018
32188
|
if (res.exceptionDetails) {
|
|
32019
|
-
return errorText(`Fill failed: ${
|
|
32189
|
+
return errorText(`Fill failed: ${explainException(res.exceptionDetails, selector)}`);
|
|
32020
32190
|
}
|
|
32021
32191
|
const info = res.result.value;
|
|
32022
32192
|
if (!info?.found) {
|
|
32023
|
-
return errorText(
|
|
32193
|
+
return errorText(import_common_tags5.oneLine`
|
|
32024
32194
|
No element matched ${JSON.stringify(selector)} for game_fill at index ${index}
|
|
32025
32195
|
(matches found: ${info?.count ?? 0}).
|
|
32026
32196
|
`);
|
|
32027
32197
|
}
|
|
32028
|
-
return text(
|
|
32198
|
+
return text(import_common_tags5.oneLine`
|
|
32029
32199
|
Filled ${JSON.stringify(selector)} [index ${index}] (${info.mode}).
|
|
32030
32200
|
Value is now ${JSON.stringify(info.value)}. Matches: ${info.count}.
|
|
32031
32201
|
`);
|
|
@@ -32040,16 +32210,16 @@ async function gameType(client, selector, textToType, index = 0) {
|
|
|
32040
32210
|
returnByValue: true
|
|
32041
32211
|
});
|
|
32042
32212
|
if (res.exceptionDetails) {
|
|
32043
|
-
return errorText(`Type failed: ${
|
|
32213
|
+
return errorText(`Type failed: ${explainException(res.exceptionDetails, selector)}`);
|
|
32044
32214
|
}
|
|
32045
32215
|
const info = res.result.value;
|
|
32046
32216
|
if (!info?.found) {
|
|
32047
|
-
return errorText(
|
|
32217
|
+
return errorText(import_common_tags5.oneLine`
|
|
32048
32218
|
No element matched ${JSON.stringify(selector)} for game_type at index ${index}
|
|
32049
32219
|
(matches found: ${info?.count ?? 0}).
|
|
32050
32220
|
`);
|
|
32051
32221
|
}
|
|
32052
|
-
return text(
|
|
32222
|
+
return text(import_common_tags5.oneLine`
|
|
32053
32223
|
Typed ${info.typed} char(s) into ${JSON.stringify(selector)} [index ${index}].
|
|
32054
32224
|
Value is now ${JSON.stringify(info.value)}. Matches: ${info.count}.
|
|
32055
32225
|
`);
|
|
@@ -32064,16 +32234,16 @@ async function gameHover(client, selector, index = 0) {
|
|
|
32064
32234
|
returnByValue: true
|
|
32065
32235
|
});
|
|
32066
32236
|
if (res.exceptionDetails) {
|
|
32067
|
-
return errorText(`Hover failed: ${
|
|
32237
|
+
return errorText(`Hover failed: ${explainException(res.exceptionDetails, selector)}`);
|
|
32068
32238
|
}
|
|
32069
32239
|
const info = res.result.value;
|
|
32070
32240
|
if (!info?.found) {
|
|
32071
|
-
return errorText(
|
|
32241
|
+
return errorText(import_common_tags5.oneLine`
|
|
32072
32242
|
No element matched ${JSON.stringify(selector)} for game_hover at index ${index}
|
|
32073
32243
|
(matches found: ${info?.count ?? 0}).
|
|
32074
32244
|
`);
|
|
32075
32245
|
}
|
|
32076
|
-
return text(
|
|
32246
|
+
return text(import_common_tags5.oneLine`
|
|
32077
32247
|
Hovered ${JSON.stringify(selector)} [index ${index}] at
|
|
32078
32248
|
(${info.x.toFixed(0)}, ${info.y.toFixed(0)}).
|
|
32079
32249
|
Dispatched: ${info.fired.join(", ")}. Matches: ${info.count}.
|
|
@@ -32108,21 +32278,21 @@ async function gameKey(client, options) {
|
|
|
32108
32278
|
returnByValue: true
|
|
32109
32279
|
});
|
|
32110
32280
|
if (res.exceptionDetails) {
|
|
32111
|
-
return errorText(`Key press failed: ${
|
|
32281
|
+
return errorText(`Key press failed: ${explainException(res.exceptionDetails, selector)}`);
|
|
32112
32282
|
}
|
|
32113
32283
|
const info = res.result.value;
|
|
32114
32284
|
if (!info) {
|
|
32115
32285
|
return errorText(`game_key returned no result.`);
|
|
32116
32286
|
}
|
|
32117
32287
|
if (!info.found) {
|
|
32118
|
-
return errorText(
|
|
32288
|
+
return errorText(import_common_tags5.oneLine`
|
|
32119
32289
|
No element matched ${JSON.stringify(selector)} for game_key at index ${index}
|
|
32120
32290
|
(matches found: ${info.count}).
|
|
32121
32291
|
`);
|
|
32122
32292
|
}
|
|
32123
32293
|
const where = info.via == "selector" ? `${JSON.stringify(selector)} [index ${index}] ${info.target}` : info.via == "activeElement" ? `the focused element ${info.target}` : `document`;
|
|
32124
32294
|
const matchNote = info.matches == null ? "" : ` Matches: ${info.matches}.`;
|
|
32125
|
-
return text(
|
|
32295
|
+
return text(import_common_tags5.oneLine`
|
|
32126
32296
|
Pressed ${keyLabel(key, { ctrl, shift, alt, meta: meta3 })} ${info.presses}x on ${where}.${matchNote}
|
|
32127
32297
|
Default prevented: ${info.defaultPrevented ? "yes" : "no"}.
|
|
32128
32298
|
`);
|
|
@@ -32644,7 +32814,7 @@ async function main() {
|
|
|
32644
32814
|
const server = new McpServer({ name: "gameface-devtools-mcp", version: VERSION });
|
|
32645
32815
|
server.registerTool("game_status", {
|
|
32646
32816
|
title: `Gameface UI status`,
|
|
32647
|
-
description:
|
|
32817
|
+
description: import_common_tags6.oneLine`
|
|
32648
32818
|
Check whether the Gameface UI debug endpoint is reachable and report the live page target,
|
|
32649
32819
|
engine info, and view-reload tracking.
|
|
32650
32820
|
Run this first when other game_* tools fail.
|
|
@@ -32653,7 +32823,7 @@ async function main() {
|
|
|
32653
32823
|
}, () => gameStatus(client, reloads));
|
|
32654
32824
|
server.registerTool("game_eval", {
|
|
32655
32825
|
title: `Evaluate JS in the Gameface UI`,
|
|
32656
|
-
description:
|
|
32826
|
+
description: import_common_tags6.oneLine`
|
|
32657
32827
|
Evaluate a JavaScript expression in the running Gameface UI (CDP Runtime.evaluate,
|
|
32658
32828
|
returnByValue) and return the resulting value as JSON.
|
|
32659
32829
|
`,
|
|
@@ -32664,7 +32834,7 @@ async function main() {
|
|
|
32664
32834
|
}, ({ expression, awaitPromise }) => gameEval(client, expression, awaitPromise));
|
|
32665
32835
|
server.registerTool("game_screenshot", {
|
|
32666
32836
|
title: `Screenshot the Gameface UI`,
|
|
32667
|
-
description:
|
|
32837
|
+
description: import_common_tags6.oneLine`
|
|
32668
32838
|
Capture a screenshot of the Gameface viewport and return it as an inline image; a selector
|
|
32669
32839
|
clips the capture to one element.
|
|
32670
32840
|
Clipping is the only lever on what the image costs you in context: that cost tracks the
|
|
@@ -32674,7 +32844,7 @@ async function main() {
|
|
|
32674
32844
|
`,
|
|
32675
32845
|
inputSchema: {
|
|
32676
32846
|
format: exports_external.enum(["png", "jpeg"]).optional().describe(`Image format (default: png)`),
|
|
32677
|
-
quality: exports_external.number().min(1).max(100).optional().describe(
|
|
32847
|
+
quality: exports_external.number().min(1).max(100).optional().describe(import_common_tags6.oneLine`
|
|
32678
32848
|
JPEG quality 1-100 (only used when format is jpeg; default 80).
|
|
32679
32849
|
Trades fidelity for transfer bytes, leaving the image's context cost unchanged.
|
|
32680
32850
|
`),
|
|
@@ -32684,7 +32854,7 @@ async function main() {
|
|
|
32684
32854
|
}, ({ format, quality, selector, index }) => gameScreenshot(client, debug, { format, quality, selector, index }));
|
|
32685
32855
|
server.registerTool("game_wait", {
|
|
32686
32856
|
title: `Wait for a condition in the Gameface UI`,
|
|
32687
|
-
description:
|
|
32857
|
+
description: import_common_tags6.oneLine`
|
|
32688
32858
|
Wait until a CSS selector matches (optionally visible), a JS predicate becomes truthy,
|
|
32689
32859
|
and/or a view reload happens. Provide at least one of reload / selector / predicate
|
|
32690
32860
|
(selector and predicate are mutually exclusive).
|
|
@@ -32695,20 +32865,20 @@ async function main() {
|
|
|
32695
32865
|
inputSchema: {
|
|
32696
32866
|
selector: exports_external.string().optional().describe(`CSS selector to wait for`),
|
|
32697
32867
|
predicate: exports_external.string().optional().describe(`JS expression evaluated in the page; waits until it is truthy`),
|
|
32698
|
-
reload: exports_external.boolean().optional().describe(
|
|
32868
|
+
reload: exports_external.boolean().optional().describe(import_common_tags6.oneLine`
|
|
32699
32869
|
Wait for a view reload (context reset) before the selector/predicate phase.
|
|
32700
32870
|
Without sinceReloads, waits for the next reload after the call starts.
|
|
32701
32871
|
`),
|
|
32702
|
-
sinceReloads: exports_external.number().int().min(0).optional().describe(
|
|
32872
|
+
sinceReloads: exports_external.number().int().min(0).optional().describe(import_common_tags6.oneLine`
|
|
32703
32873
|
Baseline reload count (from a prior game_status or game_wait).
|
|
32704
32874
|
The reload phase is satisfied as soon as the count exceeds it, even if the reload
|
|
32705
32875
|
already happened; use it to avoid racing a reload you triggered yourself.
|
|
32706
32876
|
`),
|
|
32707
|
-
quiescentMs: exports_external.number().int().min(0).optional().describe(
|
|
32877
|
+
quiescentMs: exports_external.number().int().min(0).optional().describe(import_common_tags6.oneLine`
|
|
32708
32878
|
After a reload is observed, hold until no further context swap for this long (default
|
|
32709
32879
|
1000, 0 disables); absorbs engines that swap the context several times per reload.
|
|
32710
32880
|
`),
|
|
32711
|
-
timeoutMs: exports_external.number().int().min(0).optional().describe(
|
|
32881
|
+
timeoutMs: exports_external.number().int().min(0).optional().describe(import_common_tags6.oneLine`
|
|
32712
32882
|
Max time to wait in ms (default 8000, or 30000 when reload is set; capped at 60000)
|
|
32713
32883
|
`),
|
|
32714
32884
|
visible: exports_external.boolean().optional().describe(`For selector waits, also require a non-zero bounding box (default false)`)
|
|
@@ -32724,7 +32894,7 @@ async function main() {
|
|
|
32724
32894
|
}));
|
|
32725
32895
|
server.registerTool("game_fill", {
|
|
32726
32896
|
title: `Set an input value in the Gameface UI`,
|
|
32727
|
-
description:
|
|
32897
|
+
description: import_common_tags6.oneLine`
|
|
32728
32898
|
Set the value of an input, textarea, or contenteditable element and fire input/change so
|
|
32729
32899
|
the UI framework reacts as if the user edited it.
|
|
32730
32900
|
Best for setting a field in one shot; use game_type for keystrokes.
|
|
@@ -32737,7 +32907,7 @@ async function main() {
|
|
|
32737
32907
|
}, ({ selector, value, index }) => gameFill(client, selector, value, index));
|
|
32738
32908
|
server.registerTool("game_type", {
|
|
32739
32909
|
title: `Type text into the Gameface UI`,
|
|
32740
|
-
description:
|
|
32910
|
+
description: import_common_tags6.oneLine`
|
|
32741
32911
|
Type text into an element character by character, firing real KeyboardEvents plus keeping
|
|
32742
32912
|
the value in sync.
|
|
32743
32913
|
Use when handlers react to individual keystrokes; otherwise game_fill.
|
|
@@ -32750,7 +32920,7 @@ async function main() {
|
|
|
32750
32920
|
}, ({ selector, text: text2, index }) => gameType(client, selector, text2, index));
|
|
32751
32921
|
server.registerTool("game_hover", {
|
|
32752
32922
|
title: `Hover an element in the Gameface UI`,
|
|
32753
|
-
description:
|
|
32923
|
+
description: import_common_tags6.oneLine`
|
|
32754
32924
|
Hover an element by dispatching the pointer/mouse over/enter/move sequence in the page, so
|
|
32755
32925
|
the UI's mouseenter / pointerover JS handlers (tooltips) fire.
|
|
32756
32926
|
The CSS :hover state is NOT set (only real game-forwarded mouse input sets it); verify a
|
|
@@ -32763,7 +32933,7 @@ async function main() {
|
|
|
32763
32933
|
}, ({ selector, index }) => gameHover(client, selector, index));
|
|
32764
32934
|
server.registerTool("game_key", {
|
|
32765
32935
|
title: `Press a key in the Gameface UI`,
|
|
32766
|
-
description:
|
|
32936
|
+
description: import_common_tags6.oneLine`
|
|
32767
32937
|
Press a named key by dispatching a real bubbling keydown+keyup (no keypress) in the page
|
|
32768
32938
|
(KeyboardEvent.key, e.g. Escape, Enter, ArrowDown, a, F5), optionally with
|
|
32769
32939
|
ctrl/shift/alt/meta and a repeat count.
|
|
@@ -32788,7 +32958,7 @@ async function main() {
|
|
|
32788
32958
|
}, ({ key, count, ctrl, shift, alt, meta: meta3, selector, index }) => gameKey(client, { key, count, ctrl, shift, alt, meta: meta3, selector, index }));
|
|
32789
32959
|
server.registerTool("game_console", {
|
|
32790
32960
|
title: `Read the Gameface UI console`,
|
|
32791
|
-
description:
|
|
32961
|
+
description: import_common_tags6.oneLine`
|
|
32792
32962
|
Return recent console.* calls, log entries, and uncaught exceptions captured from the
|
|
32793
32963
|
Gameface UI, each prefixed with local wall-clock time and its object arguments expanded to
|
|
32794
32964
|
their real values (state {a: 1, b: {c: {…}}, arr: [1, 2, 3]}).
|
|
@@ -32800,13 +32970,13 @@ async function main() {
|
|
|
32800
32970
|
inputSchema: {
|
|
32801
32971
|
limit: exports_external.number().int().min(1).max(1000).optional().describe(`Max entries to return (default 50)`),
|
|
32802
32972
|
level: exports_external.string().optional().describe(`Filter by level, e.g. error / warning / log / info`),
|
|
32803
|
-
depth: exports_external.number().int().min(1).max(CAPTURE_DEPTH_CAP).optional().describe(
|
|
32973
|
+
depth: exports_external.number().int().min(1).max(CAPTURE_DEPTH_CAP).optional().describe(import_common_tags6.oneLine`
|
|
32804
32974
|
How many levels of an expanded object to render (default
|
|
32805
32975
|
${String(DEFAULT_RENDER_DEPTH)}).
|
|
32806
32976
|
Entries are captured ${String(CAPTURE_DEPTH_CAP)} levels deep, so re-reading the same
|
|
32807
32977
|
entries deeper works up to that cap.
|
|
32808
32978
|
`),
|
|
32809
|
-
clear: exports_external.boolean().optional().describe(
|
|
32979
|
+
clear: exports_external.boolean().optional().describe(import_common_tags6.oneLine`
|
|
32810
32980
|
Empty the buffer once this read has taken its entries (default false).
|
|
32811
32981
|
It also drops captures still being expanded, so a line logged moments before the call
|
|
32812
32982
|
is discarded rather than surfacing on the next read.
|
|
@@ -32815,7 +32985,7 @@ async function main() {
|
|
|
32815
32985
|
}, ({ limit, level, depth, clear }) => gameConsole(client, consoleBuffer, { limit, level, depth, clear }));
|
|
32816
32986
|
server.registerTool("game_dom", {
|
|
32817
32987
|
title: `Inspect Gameface UI DOM`,
|
|
32818
|
-
description:
|
|
32988
|
+
description: import_common_tags6.oneLine`
|
|
32819
32989
|
Return DOM details (tag, id, classes, attributes, bounding rect, outerHTML) for elements
|
|
32820
32990
|
matching a CSS selector in the live Gameface UI.
|
|
32821
32991
|
outerHTML dominates the response: to locate elements, get a targetable handle, or read
|
|
@@ -32829,7 +32999,7 @@ async function main() {
|
|
|
32829
32999
|
}, ({ selector, all, maxHtml }) => gameDom(client, selector, all, maxHtml));
|
|
32830
33000
|
server.registerTool("game_query", {
|
|
32831
33001
|
title: `Find elements in the Gameface UI`,
|
|
32832
|
-
description:
|
|
33002
|
+
description: import_common_tags6.oneLine`
|
|
32833
33003
|
Locate elements in the live Gameface UI by CSS selector, by trimmed textContent
|
|
32834
33004
|
(equals/contains/regex, case-insensitive by default), or by both; one of the two is
|
|
32835
33005
|
required, and match/caseSensitive are ignored without text.
|
|
@@ -32839,7 +33009,7 @@ async function main() {
|
|
|
32839
33009
|
To read an element's markup, use game_dom.
|
|
32840
33010
|
`,
|
|
32841
33011
|
inputSchema: {
|
|
32842
|
-
text: exports_external.string().optional().describe(
|
|
33012
|
+
text: exports_external.string().optional().describe(import_common_tags6.oneLine`
|
|
32843
33013
|
Text to match against each element's trimmed textContent, so pass it trimmed under
|
|
32844
33014
|
match=equals; omit it to select on the selector alone, or pass '' with match=equals
|
|
32845
33015
|
and a selector for elements carrying no text.
|
|
@@ -32847,11 +33017,11 @@ async function main() {
|
|
|
32847
33017
|
match: exports_external.enum(["equals", "contains", "regex"]).optional().describe(`How to match the text: equals / contains / regex (default: contains)`),
|
|
32848
33018
|
caseSensitive: exports_external.boolean().optional().describe(`Match case-sensitively (default: false)`),
|
|
32849
33019
|
selector: exports_external.string().optional().describe(`CSS selector to match, alone or scoping the text scan (with text, defaults to *)`),
|
|
32850
|
-
deepest: exports_external.boolean().optional().describe(
|
|
33020
|
+
deepest: exports_external.boolean().optional().describe(import_common_tags6.oneLine`
|
|
32851
33021
|
Keep only the innermost match, pruning ancestors that also matched (default: true
|
|
32852
33022
|
when non-empty text drives the match, false otherwise).
|
|
32853
33023
|
`),
|
|
32854
|
-
tag: exports_external.boolean().optional().describe(
|
|
33024
|
+
tag: exports_external.boolean().optional().describe(import_common_tags6.oneLine`
|
|
32855
33025
|
Stamp matches with data-gf-tag handles and return them as selectors, clearing any
|
|
32856
33026
|
prior handles first (default: false).
|
|
32857
33027
|
`),
|
|
@@ -32861,7 +33031,7 @@ async function main() {
|
|
|
32861
33031
|
}, ({ text: text2, match, caseSensitive, selector, deepest, tag, attributes, limit }) => gameQuery(client, { text: text2, match, caseSensitive, selector, deepest, tag, attributes, limit }));
|
|
32862
33032
|
server.registerTool("game_click", {
|
|
32863
33033
|
title: `Click an element in the Gameface UI`,
|
|
32864
|
-
description:
|
|
33034
|
+
description: import_common_tags6.oneLine`
|
|
32865
33035
|
Click the element matching a CSS selector by dispatching a real bubbling pointer/mouse/click
|
|
32866
33036
|
sequence in the page (NOT CDP Input, which Gameface ignores for the UI).
|
|
32867
33037
|
`,
|
|
@@ -32872,7 +33042,7 @@ async function main() {
|
|
|
32872
33042
|
}, ({ selector, index }) => gameClick(client, selector, index));
|
|
32873
33043
|
server.registerTool("game_debug_status", {
|
|
32874
33044
|
title: `JS debugger status`,
|
|
32875
|
-
description:
|
|
33045
|
+
description: import_common_tags6.oneLine`
|
|
32876
33046
|
Report debugger state: whether paused (and where), pause-on-exceptions mode, breakpoints,
|
|
32877
33047
|
and parsed script count.
|
|
32878
33048
|
Enables the debugger on first use.
|
|
@@ -32883,7 +33053,7 @@ async function main() {
|
|
|
32883
33053
|
}, ({ setPauseOnExceptions }) => debug.status(setPauseOnExceptions));
|
|
32884
33054
|
server.registerTool("game_debug_scripts", {
|
|
32885
33055
|
title: `List parsed UI scripts`,
|
|
32886
|
-
description:
|
|
33056
|
+
description: import_common_tags6.oneLine`
|
|
32887
33057
|
List JavaScript scripts parsed in the Gameface UI (scriptId + url + line count).
|
|
32888
33058
|
Only scripts parsed since the debugger attached appear, since Gameface does not replay
|
|
32889
33059
|
scriptParsed; an empty list is what a late attach looks like, and the result says how to
|
|
@@ -32897,7 +33067,7 @@ async function main() {
|
|
|
32897
33067
|
}, ({ filter }) => debug.listScripts(filter));
|
|
32898
33068
|
server.registerTool("game_debug_source", {
|
|
32899
33069
|
title: `Get UI script source`,
|
|
32900
|
-
description:
|
|
33070
|
+
description: import_common_tags6.oneLine`
|
|
32901
33071
|
Return the source of a script (by scriptId from game_debug_scripts), with line numbers.
|
|
32902
33072
|
The window is capped at 400 lines, range or not; the result says which lines it showed.
|
|
32903
33073
|
It renders whole lines, so a low line count is no promise of a small answer: one minified
|
|
@@ -32906,12 +33076,12 @@ async function main() {
|
|
|
32906
33076
|
`,
|
|
32907
33077
|
inputSchema: {
|
|
32908
33078
|
scriptId: exports_external.string().describe(`Script id from game_debug_scripts`),
|
|
32909
|
-
lineStart: exports_external.number().int().min(1).optional().describe(
|
|
33079
|
+
lineStart: exports_external.number().int().min(1).optional().describe(import_common_tags6.oneLine`
|
|
32910
33080
|
First line, 1-based and numbered as the whole file or document is, so a script embedded
|
|
32911
33081
|
in a page starts at its firstLine from game_debug_scripts rather than at 1.
|
|
32912
33082
|
`),
|
|
32913
33083
|
lineEnd: exports_external.number().int().min(1).optional().describe(`Last line, numbered as lineStart is`),
|
|
32914
|
-
maxChars: exports_external.number().int().min(1).optional().describe(
|
|
33084
|
+
maxChars: exports_external.number().int().min(1).optional().describe(import_common_tags6.oneLine`
|
|
32915
33085
|
Characters of source to return before clipping (default
|
|
32916
33086
|
${String(DEFAULT_SOURCE_MAX_CHARS)}); the result says when it clipped and at what.
|
|
32917
33087
|
`)
|
|
@@ -32919,7 +33089,7 @@ async function main() {
|
|
|
32919
33089
|
}, ({ scriptId, lineStart, lineEnd, maxChars }) => debug.getSource(scriptId, lineStart, lineEnd, maxChars));
|
|
32920
33090
|
server.registerTool("game_debug_search_source", {
|
|
32921
33091
|
title: `Search UI script sources`,
|
|
32922
|
-
description:
|
|
33092
|
+
description: import_common_tags6.oneLine`
|
|
32923
33093
|
Find a literal string across the parsed script sources and return each hit as
|
|
32924
33094
|
url + line + column (1-based) with a snippet of surrounding source.
|
|
32925
33095
|
The query is case-sensitive and literal, no regex; read the total/truncated fields for what
|
|
@@ -32935,14 +33105,14 @@ async function main() {
|
|
|
32935
33105
|
}, ({ query, urlContains }) => debug.searchSource(query, urlContains));
|
|
32936
33106
|
server.registerTool("game_debug_set_breakpoint", {
|
|
32937
33107
|
title: `Set a breakpoint`,
|
|
32938
|
-
description:
|
|
33108
|
+
description: import_common_tags6.oneLine`
|
|
32939
33109
|
Set a breakpoint by url substring + line, and optionally column; both are 1-based.
|
|
32940
33110
|
A condition limits how often the UI freezes; a line alone can bind somewhere that never
|
|
32941
33111
|
runs again, so check the resolved location the result reports back.
|
|
32942
33112
|
Hitting it FREEZES the UI until you resume with game_debug_step.
|
|
32943
33113
|
`,
|
|
32944
33114
|
inputSchema: {
|
|
32945
|
-
urlContains: exports_external.string().describe(
|
|
33115
|
+
urlContains: exports_external.string().describe(import_common_tags6.oneLine`
|
|
32946
33116
|
Substring of the script url to break in, matched case-sensitively; copy it from the url
|
|
32947
33117
|
game_debug_scripts or game_debug_search_source printed, whose own filters are not.
|
|
32948
33118
|
`),
|
|
@@ -32960,7 +33130,7 @@ async function main() {
|
|
|
32960
33130
|
}, ({ breakpoint }) => debug.removeBreakpoint(breakpoint));
|
|
32961
33131
|
server.registerTool("game_debug_pause_state", {
|
|
32962
33132
|
title: `Inspect the paused stack`,
|
|
32963
|
-
description:
|
|
33133
|
+
description: import_common_tags6.oneLine`
|
|
32964
33134
|
When paused, return the call stack (frames with function + location + scope types);
|
|
32965
33135
|
'not paused' otherwise.
|
|
32966
33136
|
`,
|
|
@@ -32970,7 +33140,7 @@ async function main() {
|
|
|
32970
33140
|
}, ({ expandScopes }) => debug.pauseStateReport(expandScopes ?? false));
|
|
32971
33141
|
server.registerTool("game_debug_evaluate", {
|
|
32972
33142
|
title: `Evaluate while debugging`,
|
|
32973
|
-
description:
|
|
33143
|
+
description: import_common_tags6.oneLine`
|
|
32974
33144
|
Evaluate a JS expression.
|
|
32975
33145
|
When paused, it runs in the selected call frame's scope so you can read locals; otherwise
|
|
32976
33146
|
it runs globally.
|
|
@@ -32983,7 +33153,7 @@ async function main() {
|
|
|
32983
33153
|
}, ({ expression, frameIndex }) => debug.evaluate(expression, frameIndex));
|
|
32984
33154
|
server.registerTool("game_debug_step", {
|
|
32985
33155
|
title: `Step / resume / pause execution`,
|
|
32986
|
-
description:
|
|
33156
|
+
description: import_common_tags6.oneLine`
|
|
32987
33157
|
Control paused execution: resume (unfreeze the UI), over/into/out (step), or pause (break at
|
|
32988
33158
|
the next statement).
|
|
32989
33159
|
Stepping reports the new location, and resume fails rather than claiming success when the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@csmodding/gameface-devtools-mcp",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.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",
|