mcp-unknowncheatz 0.3.1 → 0.3.2
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 +1 -1
- package/package.json +1 -1
- package/src/browser.ts +39 -16
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ bun install --frozen-lockfile
|
|
|
23
23
|
bun run start
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
The server uses MCP over standard input and output. Chrome opens when a tool first needs a page. On Linux without a graphical display,
|
|
26
|
+
The server uses MCP over standard input and output. Chrome opens when a tool first needs a page. It keeps a dedicated profile under the user's application data directory (`mcp-unknowncheat/chrome-profile`) so a manually completed browser challenge and login can survive restarts. Set `UC_PROFILE_DIR` to an absolute path to choose another profile; an existing `cookies.json` is imported only when the profile is first created. On Linux without a graphical display, Chrome runs headless; set `UC_HEADLESS=1` to request headless mode elsewhere. If a Cloudflare challenge appears, complete it in the visible Chrome window. The server waits up to 45 seconds by default (`UC_CF_WAIT_MS`), subject to each tool's time budget. Automated browsers are not guaranteed to pass production challenges. Clients may need a tool timeout over 60 seconds for first-time manual setup.
|
|
27
27
|
|
|
28
28
|
## Tools
|
|
29
29
|
|
package/package.json
CHANGED
package/src/browser.ts
CHANGED
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
import puppeteer, { type Browser, type Page } from "puppeteer-core";
|
|
2
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
2
4
|
import path from "path";
|
|
3
5
|
import { fileURLToPath } from "url";
|
|
4
6
|
|
|
5
7
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
8
|
const COOKIES_PATH = path.join(__dirname, "..", "cookies.json");
|
|
9
|
+
const DATA_DIR = process.platform === "win32"
|
|
10
|
+
? path.join(process.env.LOCALAPPDATA ?? os.homedir(), "mcp-unknowncheat")
|
|
11
|
+
: path.join(process.env.XDG_DATA_HOME ?? path.join(os.homedir(), ".local", "share"), "mcp-unknowncheat");
|
|
12
|
+
const PROFILE_DIR = process.env.UC_PROFILE_DIR ?? path.join(DATA_DIR, "chrome-profile");
|
|
7
13
|
const CLOUDFLARE_INDICATORS = ["Just a moment", "cf-browser-verification", "Checking your browser"];
|
|
8
14
|
const NAV_TIMEOUT = 30_000;
|
|
9
15
|
const NAV_TIMEOUT_RETRY = 60_000;
|
|
10
|
-
const CF_WAIT_MS = Number(process.env.UC_CF_WAIT_MS ??
|
|
16
|
+
const CF_WAIT_MS = Number(process.env.UC_CF_WAIT_MS ?? 45_000);
|
|
11
17
|
|
|
12
18
|
function useRealDisplay(): boolean {
|
|
13
19
|
return !!(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
|
|
14
20
|
}
|
|
15
21
|
|
|
22
|
+
function useHeadless(): boolean {
|
|
23
|
+
return process.env.UC_HEADLESS === "1" || (process.platform !== "win32" && !useRealDisplay());
|
|
24
|
+
}
|
|
25
|
+
|
|
16
26
|
const ALLOWED_HOSTS = new Set(["www.unknowncheats.me", "unknowncheats.me"]);
|
|
17
27
|
|
|
18
28
|
export function validateUrl(url: string): void {
|
|
@@ -89,11 +99,14 @@ async function launchBrowser(): Promise<BrowserInstance> {
|
|
|
89
99
|
console.error("[browser] Launching Chrome...");
|
|
90
100
|
const onWayland = process.env.XDG_SESSION_TYPE === "wayland" || !!process.env.WAYLAND_DISPLAY;
|
|
91
101
|
const executablePath = process.env.UC_CHROME_PATH?.trim();
|
|
102
|
+
const existingProfile = existsSync(PROFILE_DIR);
|
|
103
|
+
mkdirSync(PROFILE_DIR, { recursive: true });
|
|
92
104
|
const browser = await puppeteer.launch({
|
|
93
105
|
...(executablePath ? { executablePath } : { channel: "chrome" as const }),
|
|
94
|
-
headless:
|
|
106
|
+
headless: useHeadless(),
|
|
95
107
|
args: onWayland ? ["--ozone-platform=wayland", "--start-maximized"] : ["--start-maximized"],
|
|
96
108
|
defaultViewport: null,
|
|
109
|
+
userDataDir: PROFILE_DIR,
|
|
97
110
|
});
|
|
98
111
|
const page = await browser.newPage();
|
|
99
112
|
|
|
@@ -102,7 +115,7 @@ async function launchBrowser(): Promise<BrowserInstance> {
|
|
|
102
115
|
instance = null;
|
|
103
116
|
});
|
|
104
117
|
|
|
105
|
-
await loadCookies(page);
|
|
118
|
+
if (!existingProfile) await loadCookies(page);
|
|
106
119
|
return { browser, page };
|
|
107
120
|
}
|
|
108
121
|
|
|
@@ -130,6 +143,26 @@ function hasCloudflareChallenge(html: string): boolean {
|
|
|
130
143
|
return CLOUDFLARE_INDICATORS.some((indicator) => html.includes(indicator));
|
|
131
144
|
}
|
|
132
145
|
|
|
146
|
+
async function waitForChallenge(page: Page, initialHtml: string, deadlineAt?: number): Promise<string> {
|
|
147
|
+
if (!hasCloudflareChallenge(initialHtml)) return initialHtml;
|
|
148
|
+
if (useHeadless()) {
|
|
149
|
+
throw new Error("CloudflareBlockError: A challenge appeared in headless Chrome. Use visible Chrome to complete it manually.");
|
|
150
|
+
}
|
|
151
|
+
const waitMs = Number.isFinite(CF_WAIT_MS) ? Math.max(0, CF_WAIT_MS) : 45_000;
|
|
152
|
+
const stopAt = Math.min(Date.now() + waitMs, deadlineAt ?? Infinity);
|
|
153
|
+
console.error(`[browser] Cloudflare challenge: complete it in the visible Chrome window (up to ${Math.ceil((stopAt - Date.now()) / 1000)} seconds).`);
|
|
154
|
+
while (Date.now() < stopAt) {
|
|
155
|
+
await Bun.sleep(Math.min(1_000, stopAt - Date.now()));
|
|
156
|
+
try {
|
|
157
|
+
const html = await page.content();
|
|
158
|
+
if (!hasCloudflareChallenge(html)) return html;
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (!isDetachedError(error)) throw error;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
throw new Error("CloudflareBlockError: Challenge stayed open in Chrome. Complete it manually or use forum-supported access; this server cannot guarantee automated clearance.");
|
|
164
|
+
}
|
|
165
|
+
|
|
133
166
|
function isDetachedError(err: unknown): boolean {
|
|
134
167
|
if (!(err instanceof Error)) return false;
|
|
135
168
|
const msg = err.message;
|
|
@@ -159,21 +192,11 @@ export async function navigateWithRetry(url: string, deadlineAt?: number): Promi
|
|
|
159
192
|
return Math.max(1, Math.min(maximum, left));
|
|
160
193
|
};
|
|
161
194
|
|
|
162
|
-
const attempt = async (timeout: number, waitUntil: "networkidle2" | "domcontentloaded" = "
|
|
195
|
+
const attempt = async (timeout: number, waitUntil: "networkidle2" | "domcontentloaded" = "domcontentloaded"): Promise<string> => {
|
|
163
196
|
await page.goto(url, { waitUntil, timeout: remaining(timeout) });
|
|
164
197
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if (hasCloudflareChallenge(html)) {
|
|
168
|
-
console.error("[browser] Cloudflare challenge detected, waiting", CF_WAIT_MS, "ms...");
|
|
169
|
-
await new Promise((res) => setTimeout(res, remaining(CF_WAIT_MS)));
|
|
170
|
-
remaining(1);
|
|
171
|
-
html = await page.content();
|
|
172
|
-
|
|
173
|
-
if (hasCloudflareChallenge(html)) {
|
|
174
|
-
throw new Error("CloudflareBlockError: Challenge did not resolve after waiting");
|
|
175
|
-
}
|
|
176
|
-
}
|
|
198
|
+
const html = await waitForChallenge(page, await page.content(), deadlineAt);
|
|
199
|
+
remaining(1);
|
|
177
200
|
|
|
178
201
|
await saveCookies(page);
|
|
179
202
|
return html;
|