mcp-unknowncheatz 0.3.1 → 0.3.3
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 +71 -3
- package/package.json +2 -2
- package/src/browser.ts +53 -23
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ https://github.com/user-attachments/assets/9f00f783-0a03-4e8d-b5b6-abd308936f20
|
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
11
|
-
Install [Bun](https://bun.sh) and Google Chrome. The server uses
|
|
11
|
+
Install [Bun](https://bun.sh) and Google Chrome. The server uses `puppeteer-real-browser` and your installed browser; installation does not download a browser. Set `UC_CHROME_PATH` to an absolute Chrome or Chromium executable path if Chrome is not in its standard location. Run the npm package with:
|
|
12
12
|
|
|
13
13
|
```sh
|
|
14
14
|
bunx mcp-unknowncheatz
|
|
@@ -23,7 +23,75 @@ 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
|
+
|
|
28
|
+
## Connect an MCP client
|
|
29
|
+
|
|
30
|
+
Install Bun and Chrome first. Your MCP client starts the server with `bunx mcp-unknowncheatz`; you do not need to leave a separate terminal running. The examples below use the published npm package. To use a source checkout, replace `bunx mcp-unknowncheatz` with `bun run /absolute/path/to/mcp-unknowncheat/src/index.ts`.
|
|
31
|
+
|
|
32
|
+
### Codex
|
|
33
|
+
|
|
34
|
+
Add the server from a terminal:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
codex mcp add unknowncheat -- bunx mcp-unknowncheatz
|
|
38
|
+
codex mcp list
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Or add this to `~/.codex/config.toml` (on Windows, `%USERPROFILE%\.codex\config.toml`):
|
|
42
|
+
|
|
43
|
+
```toml
|
|
44
|
+
[mcp_servers.unknowncheat]
|
|
45
|
+
command = "bunx"
|
|
46
|
+
args = ["mcp-unknowncheatz"]
|
|
47
|
+
tool_timeout_sec = 120
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Restart Codex after editing the config file.
|
|
51
|
+
|
|
52
|
+
### Claude Code
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
claude mcp add --scope user unknowncheat -- bunx mcp-unknowncheatz
|
|
56
|
+
claude mcp list
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Use `--scope project` if the server should be available only in one project.
|
|
60
|
+
|
|
61
|
+
### Claude Desktop and Cursor
|
|
62
|
+
|
|
63
|
+
Add the server entry under `mcpServers` in the client's JSON config. Claude Desktop uses `%APPDATA%\Claude\claude_desktop_config.json` on Windows or `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS. Cursor uses `~/.cursor/mcp.json` for all projects or `.cursor/mcp.json` in one project.
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"mcpServers": {
|
|
68
|
+
"unknowncheat": {
|
|
69
|
+
"command": "bunx",
|
|
70
|
+
"args": ["mcp-unknowncheatz"]
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Merge this entry into an existing `mcpServers` object if you already have other servers. Quit and reopen the client after saving the file.
|
|
77
|
+
|
|
78
|
+
### VS Code
|
|
79
|
+
|
|
80
|
+
Add this to `.vscode/mcp.json` in your workspace, then run **MCP: List Servers** from the Command Palette to start or inspect it:
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"servers": {
|
|
85
|
+
"unknowncheat": {
|
|
86
|
+
"type": "stdio",
|
|
87
|
+
"command": "bunx",
|
|
88
|
+
"args": ["mcp-unknowncheatz"]
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
For other clients, configure a local stdio MCP server with command `bunx` and argument `mcp-unknowncheatz`. If the client cannot find `bunx`, use its absolute executable path. On first use, allow time for the package to start and for any browser challenge; where supported, set a tool timeout of at least 120 seconds. Ask the client to list its MCP tools or call `check_login` to confirm the connection.
|
|
27
95
|
|
|
28
96
|
## Tools
|
|
29
97
|
|
|
@@ -78,7 +146,7 @@ The HTML inspector reads a saved page locally and reports selector counts, parse
|
|
|
78
146
|
|
|
79
147
|
| Variable | Default | Purpose |
|
|
80
148
|
|---|---:|---|
|
|
81
|
-
| `UC_CF_WAIT_MS` | `
|
|
149
|
+
| `UC_CF_WAIT_MS` | `45000` | Time to wait for a Cloudflare challenge, in milliseconds |
|
|
82
150
|
| `UC_CACHE_TTL_MS` | `300000` | HTML cache lifetime, in milliseconds |
|
|
83
151
|
| `UC_MIN_REQUEST_INTERVAL_MS` | `900` | Minimum interval between crawl requests, in milliseconds |
|
|
84
152
|
| `UC_INDEX_PATH` | User application data directory | Path of the local SQLite search index |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-unknowncheatz",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MCP server for searching and reading the UnknownCheats forum",
|
|
6
6
|
"bin": {
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@modelcontextprotocol/sdk": "^1.30.1",
|
|
45
45
|
"cheerio": "^1.2.0",
|
|
46
|
-
"puppeteer-
|
|
46
|
+
"puppeteer-real-browser": "1.4.4",
|
|
47
47
|
"zod": "^4.6.5"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
package/src/browser.ts
CHANGED
|
@@ -1,18 +1,28 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { connect } from "puppeteer-real-browser";
|
|
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 {
|
|
@@ -31,8 +41,8 @@ export function validateUrl(url: string): void {
|
|
|
31
41
|
}
|
|
32
42
|
|
|
33
43
|
type BrowserInstance = {
|
|
34
|
-
browser:
|
|
35
|
-
page:
|
|
44
|
+
browser: Awaited<ReturnType<typeof connect>>["browser"];
|
|
45
|
+
page: Awaited<ReturnType<typeof connect>>["page"];
|
|
36
46
|
};
|
|
37
47
|
|
|
38
48
|
let instance: BrowserInstance | null = null;
|
|
@@ -89,20 +99,26 @@ 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();
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
102
|
+
const existingProfile = existsSync(PROFILE_DIR);
|
|
103
|
+
mkdirSync(PROFILE_DIR, { recursive: true });
|
|
104
|
+
const { browser, page } = await connect({
|
|
105
|
+
headless: useHeadless(),
|
|
106
|
+
turnstile: true,
|
|
95
107
|
args: onWayland ? ["--ozone-platform=wayland", "--start-maximized"] : ["--start-maximized"],
|
|
96
|
-
|
|
108
|
+
customConfig: {
|
|
109
|
+
...(executablePath ? { chromePath: executablePath } : {}),
|
|
110
|
+
userDataDir: PROFILE_DIR,
|
|
111
|
+
},
|
|
112
|
+
connectOption: { defaultViewport: null },
|
|
113
|
+
disableXvfb: useRealDisplay(),
|
|
97
114
|
});
|
|
98
|
-
const page = await browser.newPage();
|
|
99
115
|
|
|
100
116
|
browser.on("disconnected", () => {
|
|
101
117
|
console.error("[browser] Browser disconnected");
|
|
102
118
|
instance = null;
|
|
103
119
|
});
|
|
104
120
|
|
|
105
|
-
await loadCookies(page);
|
|
121
|
+
if (!existingProfile) await loadCookies(page);
|
|
106
122
|
return { browser, page };
|
|
107
123
|
}
|
|
108
124
|
|
|
@@ -130,6 +146,30 @@ function hasCloudflareChallenge(html: string): boolean {
|
|
|
130
146
|
return CLOUDFLARE_INDICATORS.some((indicator) => html.includes(indicator));
|
|
131
147
|
}
|
|
132
148
|
|
|
149
|
+
function isPendingPage(html: string): boolean {
|
|
150
|
+
return html.length < 200 || hasCloudflareChallenge(html);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function waitForChallenge(page: BrowserInstance["page"], initialHtml: string, deadlineAt?: number): Promise<string> {
|
|
154
|
+
if (!isPendingPage(initialHtml)) return initialHtml;
|
|
155
|
+
if (useHeadless()) {
|
|
156
|
+
throw new Error("CloudflareBlockError: A challenge appeared in headless Chrome. Use visible Chrome to complete it manually.");
|
|
157
|
+
}
|
|
158
|
+
const waitMs = Number.isFinite(CF_WAIT_MS) ? Math.max(0, CF_WAIT_MS) : 45_000;
|
|
159
|
+
const stopAt = Math.min(Date.now() + waitMs, deadlineAt ?? Infinity);
|
|
160
|
+
console.error(`[browser] Cloudflare challenge: complete it in the visible Chrome window (up to ${Math.ceil((stopAt - Date.now()) / 1000)} seconds).`);
|
|
161
|
+
while (Date.now() < stopAt) {
|
|
162
|
+
await Bun.sleep(Math.min(1_000, stopAt - Date.now()));
|
|
163
|
+
try {
|
|
164
|
+
const html = await page.content();
|
|
165
|
+
if (!isPendingPage(html)) return html;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (!isDetachedError(error)) throw error;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
throw new Error("CloudflareBlockError: Challenge stayed open in Chrome. Complete it manually or use forum-supported access; this server cannot guarantee automated clearance.");
|
|
171
|
+
}
|
|
172
|
+
|
|
133
173
|
function isDetachedError(err: unknown): boolean {
|
|
134
174
|
if (!(err instanceof Error)) return false;
|
|
135
175
|
const msg = err.message;
|
|
@@ -159,21 +199,11 @@ export async function navigateWithRetry(url: string, deadlineAt?: number): Promi
|
|
|
159
199
|
return Math.max(1, Math.min(maximum, left));
|
|
160
200
|
};
|
|
161
201
|
|
|
162
|
-
const attempt = async (timeout: number, waitUntil: "networkidle2" | "domcontentloaded" = "
|
|
202
|
+
const attempt = async (timeout: number, waitUntil: "networkidle2" | "domcontentloaded" = "domcontentloaded"): Promise<string> => {
|
|
163
203
|
await page.goto(url, { waitUntil, timeout: remaining(timeout) });
|
|
164
204
|
|
|
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
|
-
}
|
|
205
|
+
const html = await waitForChallenge(page, await page.content(), deadlineAt);
|
|
206
|
+
remaining(1);
|
|
177
207
|
|
|
178
208
|
await saveCookies(page);
|
|
179
209
|
return html;
|