mcp-unknowncheatz 0.3.2 → 0.3.4

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.
Files changed (3) hide show
  1. package/README.md +70 -2
  2. package/package.json +2 -2
  3. package/src/browser.ts +18 -11
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 the maintained `puppeteer-core` package 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:
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
@@ -25,6 +25,74 @@ bun run start
25
25
 
26
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
+ ## 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.
95
+
28
96
  ## Tools
29
97
 
30
98
  | Tool | Purpose |
@@ -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` | `15000` | Time to wait for a Cloudflare challenge, in milliseconds |
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.2",
3
+ "version": "0.3.4",
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-core": "25.12.0",
46
+ "puppeteer-real-browser": "1.4.4",
47
47
  "zod": "^4.6.5"
48
48
  },
49
49
  "devDependencies": {
package/src/browser.ts CHANGED
@@ -1,4 +1,4 @@
1
- import puppeteer, { type Browser, type Page } from "puppeteer-core";
1
+ import { connect } from "puppeteer-real-browser";
2
2
  import { existsSync, mkdirSync } from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "path";
@@ -41,8 +41,8 @@ export function validateUrl(url: string): void {
41
41
  }
42
42
 
43
43
  type BrowserInstance = {
44
- browser: Browser;
45
- page: Page;
44
+ browser: Awaited<ReturnType<typeof connect>>["browser"];
45
+ page: Awaited<ReturnType<typeof connect>>["page"];
46
46
  };
47
47
 
48
48
  let instance: BrowserInstance | null = null;
@@ -101,14 +101,17 @@ async function launchBrowser(): Promise<BrowserInstance> {
101
101
  const executablePath = process.env.UC_CHROME_PATH?.trim();
102
102
  const existingProfile = existsSync(PROFILE_DIR);
103
103
  mkdirSync(PROFILE_DIR, { recursive: true });
104
- const browser = await puppeteer.launch({
105
- ...(executablePath ? { executablePath } : { channel: "chrome" as const }),
104
+ const { browser, page } = await connect({
106
105
  headless: useHeadless(),
106
+ turnstile: true,
107
107
  args: onWayland ? ["--ozone-platform=wayland", "--start-maximized"] : ["--start-maximized"],
108
- defaultViewport: null,
109
- userDataDir: PROFILE_DIR,
108
+ customConfig: {
109
+ ...(executablePath ? { chromePath: executablePath } : {}),
110
+ userDataDir: PROFILE_DIR,
111
+ },
112
+ connectOption: { defaultViewport: null },
113
+ disableXvfb: useRealDisplay(),
110
114
  });
111
- const page = await browser.newPage();
112
115
 
113
116
  browser.on("disconnected", () => {
114
117
  console.error("[browser] Browser disconnected");
@@ -143,8 +146,12 @@ function hasCloudflareChallenge(html: string): boolean {
143
146
  return CLOUDFLARE_INDICATORS.some((indicator) => html.includes(indicator));
144
147
  }
145
148
 
146
- async function waitForChallenge(page: Page, initialHtml: string, deadlineAt?: number): Promise<string> {
147
- if (!hasCloudflareChallenge(initialHtml)) return initialHtml;
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;
148
155
  if (useHeadless()) {
149
156
  throw new Error("CloudflareBlockError: A challenge appeared in headless Chrome. Use visible Chrome to complete it manually.");
150
157
  }
@@ -155,7 +162,7 @@ async function waitForChallenge(page: Page, initialHtml: string, deadlineAt?: nu
155
162
  await Bun.sleep(Math.min(1_000, stopAt - Date.now()));
156
163
  try {
157
164
  const html = await page.content();
158
- if (!hasCloudflareChallenge(html)) return html;
165
+ if (!isPendingPage(html)) return html;
159
166
  } catch (error) {
160
167
  if (!isDetachedError(error)) throw error;
161
168
  }