mcp-unknowncheatz 0.3.0 → 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 +18 -2
- package/package.json +4 -3
- package/src/browser.ts +79 -26
- package/src/crawl.ts +55 -7
- package/src/forum-index.ts +261 -0
- package/src/index.ts +3 -1
- package/src/inspect-html.ts +31 -0
- package/src/offset-discovery.ts +22 -0
- package/src/search-fallback.ts +6 -3
- package/src/sync-index.ts +113 -0
- package/src/tools/bulk-get-threads.ts +24 -4
- package/src/tools/check-login.ts +3 -2
- package/src/tools/crawl-subforum.ts +14 -3
- package/src/tools/debug-page.ts +3 -2
- package/src/tools/download-file.ts +3 -2
- package/src/tools/extract-code.ts +3 -2
- package/src/tools/find-latest-offsets.ts +33 -17
- package/src/tools/forum-index.ts +66 -0
- package/src/tools/get-thread.ts +45 -12
- package/src/tools/get-user-reputation.ts +3 -2
- package/src/tools/list-subforums.ts +3 -2
- package/src/tools/login.ts +3 -2
- package/src/tools/search-forum.ts +21 -5
package/src/tools/get-thread.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { withBrowserSession } from "../browser.js";
|
|
1
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
import { fetchHtml } from "../crawl.js";
|
|
4
5
|
import { parseThread } from "../parsers/thread.js";
|
|
6
|
+
import { getForumIndex } from "../forum-index.js";
|
|
5
7
|
import type { ThreadPost } from "../types.js";
|
|
6
8
|
|
|
7
9
|
const MAX_PAGES = 50;
|
|
@@ -13,11 +15,11 @@ function buildPageUrl(baseUrl: string, page: number): string {
|
|
|
13
15
|
return url.toString();
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
async function fetchImageAsBase64(url: string): Promise<{ data: string; mimeType: string } | null> {
|
|
18
|
+
async function fetchImageAsBase64(url: string, deadlineAt: number): Promise<{ data: string; mimeType: string } | null> {
|
|
17
19
|
try {
|
|
18
20
|
const res = await fetch(url, {
|
|
19
21
|
headers: { "User-Agent": "Mozilla/5.0" },
|
|
20
|
-
signal: AbortSignal.timeout(8_000),
|
|
22
|
+
signal: AbortSignal.timeout(Math.max(1, Math.min(8_000, deadlineAt - Date.now()))),
|
|
21
23
|
});
|
|
22
24
|
if (!res.ok) return null;
|
|
23
25
|
|
|
@@ -36,7 +38,7 @@ async function fetchImageAsBase64(url: string): Promise<{ data: string; mimeType
|
|
|
36
38
|
export function registerGetThread(server: McpServer): void {
|
|
37
39
|
server.tool(
|
|
38
40
|
"get_thread",
|
|
39
|
-
"
|
|
41
|
+
"Read the source UnknownCheats thread behind a game hacking or offset claim. Use latest_pages for recent posts in long-running threads; the default reads only the linked page.",
|
|
40
42
|
{
|
|
41
43
|
url: z.string().url().describe("Thread URL"),
|
|
42
44
|
fetch_all_pages: z
|
|
@@ -57,9 +59,11 @@ export function registerGetThread(server: McpServer): void {
|
|
|
57
59
|
.default(false)
|
|
58
60
|
.describe("If true, fetches post images and returns them as viewable image content (max 10 images)"),
|
|
59
61
|
},
|
|
60
|
-
|
|
62
|
+
{ readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
63
|
+
async ({ url, fetch_all_pages, latest_pages, include_images }) => withBrowserSession(async () => {
|
|
61
64
|
try {
|
|
62
|
-
const
|
|
65
|
+
const deadlineAt = Date.now() + 45_000;
|
|
66
|
+
const firstHtml = await fetchHtml(url, { deadlineAt });
|
|
63
67
|
const pageParam = Number(new URL(url).searchParams.get("page"));
|
|
64
68
|
const requestedPage = Number.isInteger(pageParam) && pageParam > 0 ? pageParam : 1;
|
|
65
69
|
const firstPage = parseThread(firstHtml, url, requestedPage);
|
|
@@ -72,20 +76,42 @@ export function registerGetThread(server: McpServer): void {
|
|
|
72
76
|
: [Math.min(requestedPage, totalPages)];
|
|
73
77
|
|
|
74
78
|
const allPosts: ThreadPost[] = [];
|
|
79
|
+
const pagesFetched: number[] = [];
|
|
80
|
+
let timeBudgetReached = false;
|
|
75
81
|
for (const pageNum of pagesToFetch) {
|
|
82
|
+
if (pageNum !== requestedPage && Date.now() >= deadlineAt) {
|
|
83
|
+
timeBudgetReached = true;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
76
86
|
const pageUrl = buildPageUrl(url, pageNum);
|
|
77
|
-
|
|
78
|
-
|
|
87
|
+
let html: string;
|
|
88
|
+
try {
|
|
89
|
+
html = pageNum === requestedPage ? firstHtml : await fetchHtml(pageUrl, { deadlineAt });
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (Date.now() < deadlineAt) throw error;
|
|
92
|
+
timeBudgetReached = true;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
const parsed = parseThread(html, pageUrl, pageNum);
|
|
96
|
+
allPosts.push(...parsed.posts);
|
|
97
|
+
getForumIndex().recordThreadPage(parsed, pageNum);
|
|
98
|
+
pagesFetched.push(pageNum);
|
|
79
99
|
console.error(`[get-thread] Fetched page ${pageNum}/${totalPages}`);
|
|
80
100
|
}
|
|
101
|
+
if (pagesFetched.length === 0 && firstPage.posts.length > 0) {
|
|
102
|
+
allPosts.push(...firstPage.posts);
|
|
103
|
+
pagesFetched.push(requestedPage);
|
|
104
|
+
getForumIndex().recordThreadPage(firstPage, requestedPage);
|
|
105
|
+
}
|
|
81
106
|
|
|
82
107
|
const result = {
|
|
83
108
|
title: firstPage.title,
|
|
84
109
|
posts: allPosts,
|
|
85
|
-
currentPage:
|
|
86
|
-
pagesFetched
|
|
110
|
+
currentPage: pagesFetched.at(-1),
|
|
111
|
+
pagesFetched,
|
|
87
112
|
totalPages,
|
|
88
113
|
url,
|
|
114
|
+
timeBudgetReached,
|
|
89
115
|
...(fetch_all_pages && !latest_pages && totalPages > MAX_PAGES
|
|
90
116
|
? { note: `Capped at ${MAX_PAGES} pages (thread has ${totalPages} total)` }
|
|
91
117
|
: {}),
|
|
@@ -95,7 +121,7 @@ export function registerGetThread(server: McpServer): void {
|
|
|
95
121
|
const content: Array<
|
|
96
122
|
| { type: "text"; text: string }
|
|
97
123
|
| { type: "image"; data: string; mimeType: string }
|
|
98
|
-
> = [
|
|
124
|
+
> = [];
|
|
99
125
|
|
|
100
126
|
if (include_images) {
|
|
101
127
|
// Collect all unique image URLs from all posts
|
|
@@ -116,7 +142,11 @@ export function registerGetThread(server: McpServer): void {
|
|
|
116
142
|
console.error(`[get-thread] Fetching ${imageUrls.length} images...`);
|
|
117
143
|
|
|
118
144
|
for (const imgUrl of imageUrls) {
|
|
119
|
-
|
|
145
|
+
if (Date.now() >= deadlineAt) {
|
|
146
|
+
result.timeBudgetReached = true;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
const img = await fetchImageAsBase64(imgUrl, deadlineAt);
|
|
120
150
|
if (img) {
|
|
121
151
|
content.push({ type: "image", data: img.data, mimeType: img.mimeType });
|
|
122
152
|
console.error(`[get-thread] Fetched image: ${imgUrl}`);
|
|
@@ -124,6 +154,9 @@ export function registerGetThread(server: McpServer): void {
|
|
|
124
154
|
}
|
|
125
155
|
}
|
|
126
156
|
|
|
157
|
+
if (include_images && Date.now() >= deadlineAt) result.timeBudgetReached = true;
|
|
158
|
+
content.unshift({ type: "text", text: JSON.stringify(result) });
|
|
159
|
+
|
|
127
160
|
return { content };
|
|
128
161
|
} catch (err) {
|
|
129
162
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -132,6 +165,6 @@ export function registerGetThread(server: McpServer): void {
|
|
|
132
165
|
isError: true,
|
|
133
166
|
};
|
|
134
167
|
}
|
|
135
|
-
}
|
|
168
|
+
})
|
|
136
169
|
);
|
|
137
170
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withBrowserSession } from "../browser.js";
|
|
1
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
import { load } from "cheerio";
|
|
@@ -32,7 +33,7 @@ export function registerGetUserReputation(server: McpServer): void {
|
|
|
32
33
|
.optional()
|
|
33
34
|
.describe("Numeric UC user ID (alternative to profile_url)"),
|
|
34
35
|
},
|
|
35
|
-
async ({ profile_url, user_id }) => {
|
|
36
|
+
async ({ profile_url, user_id }) => withBrowserSession(async () => {
|
|
36
37
|
try {
|
|
37
38
|
const url = profile_url ?? (user_id
|
|
38
39
|
? `https://www.unknowncheats.me/forum/members/${user_id}.html`
|
|
@@ -120,6 +121,6 @@ export function registerGetUserReputation(server: McpServer): void {
|
|
|
120
121
|
isError: true,
|
|
121
122
|
};
|
|
122
123
|
}
|
|
123
|
-
}
|
|
124
|
+
})
|
|
124
125
|
);
|
|
125
126
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withBrowserSession } from "../browser.js";
|
|
1
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
import { fetchHtml } from "../crawl.js";
|
|
@@ -12,7 +13,7 @@ export function registerListSubforums(server: McpServer): void {
|
|
|
12
13
|
limit: z.number().int().min(1).max(500).optional().default(100).describe("Max subforums returned (default 100)"),
|
|
13
14
|
refresh: z.boolean().optional().default(false).describe("Fetch the forum index again instead of using the local directory"),
|
|
14
15
|
},
|
|
15
|
-
async ({ query, limit, refresh }) => {
|
|
16
|
+
async ({ query, limit, refresh }) => withBrowserSession(async () => {
|
|
16
17
|
try {
|
|
17
18
|
const cached = refresh ? null : await readForumCatalog();
|
|
18
19
|
const catalog = cached ?? await saveForumCatalog(await fetchHtml(FORUM_INDEX, { bypassCache: refresh }));
|
|
@@ -54,6 +55,6 @@ export function registerListSubforums(server: McpServer): void {
|
|
|
54
55
|
isError: true,
|
|
55
56
|
};
|
|
56
57
|
}
|
|
57
|
-
}
|
|
58
|
+
})
|
|
58
59
|
);
|
|
59
60
|
}
|
package/src/tools/login.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withBrowserSession } from "../browser.js";
|
|
1
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
import { load } from "cheerio";
|
|
@@ -15,7 +16,7 @@ export function registerLogin(server: McpServer): void {
|
|
|
15
16
|
username: z.string().describe("UnknownCheats username"),
|
|
16
17
|
password: z.string().describe("UnknownCheats password"),
|
|
17
18
|
},
|
|
18
|
-
async ({ username, password }) => {
|
|
19
|
+
async ({ username, password }) => withBrowserSession(async () => {
|
|
19
20
|
try {
|
|
20
21
|
const { page } = await navigateWithRetry(UC_LOGIN);
|
|
21
22
|
|
|
@@ -66,6 +67,6 @@ export function registerLogin(server: McpServer): void {
|
|
|
66
67
|
isError: true,
|
|
67
68
|
};
|
|
68
69
|
}
|
|
69
|
-
}
|
|
70
|
+
})
|
|
70
71
|
);
|
|
71
72
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withBrowserSession } from "../browser.js";
|
|
1
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
import { load } from "cheerio";
|
|
@@ -6,13 +7,24 @@ import { fetchHtml } from "../crawl.js";
|
|
|
6
7
|
import { isLoggedIn } from "../auth.js";
|
|
7
8
|
import { searchViaSubforums } from "../search-fallback.js";
|
|
8
9
|
import { filterThreads, parseThreadList } from "../parsers/thread-list.js";
|
|
10
|
+
import { FORUM_INDEX, readForumCatalog, saveForumCatalog } from "../forum-catalog.js";
|
|
11
|
+
import { getForumIndex } from "../forum-index.js";
|
|
9
12
|
|
|
10
13
|
const UC_HOME = "https://www.unknowncheats.me/forum/";
|
|
11
14
|
const UC_SEARCH = "https://www.unknowncheats.me/forum/search.php";
|
|
12
15
|
|
|
13
16
|
async function runFallbackSearch(query: string) {
|
|
14
17
|
console.error(`[search] Not logged in — scanning UC subforums for "${query}"`);
|
|
15
|
-
const
|
|
18
|
+
const catalog = await readForumCatalog() ?? await saveForumCatalog(await fetchHtml(FORUM_INDEX));
|
|
19
|
+
const { results, scannedSubforums } = await searchViaSubforums(query, async (url) => {
|
|
20
|
+
const html = await fetchHtml(url);
|
|
21
|
+
const slug = new URL(url).pathname.match(/^\/forum\/([a-z0-9-]+)\/$/)?.[1];
|
|
22
|
+
if (slug) {
|
|
23
|
+
const entries = parseThreadList(html);
|
|
24
|
+
if (entries.length > 0) getForumIndex().recordListing(slug, 1, entries);
|
|
25
|
+
}
|
|
26
|
+
return html;
|
|
27
|
+
}, catalog.subforums);
|
|
16
28
|
|
|
17
29
|
return {
|
|
18
30
|
count: results.length,
|
|
@@ -29,7 +41,7 @@ async function runFallbackSearch(query: string) {
|
|
|
29
41
|
export function registerSearchForum(server: McpServer): void {
|
|
30
42
|
server.tool(
|
|
31
43
|
"search_forum",
|
|
32
|
-
"
|
|
44
|
+
"Use for live UnknownCheats evidence when a user asks about game hacking, cheats, anti-cheat, reversing, offsets, or a forum thread. Uses advanced search when logged in, scans relevant subforums as a guest, or browses a named subforum.",
|
|
33
45
|
{
|
|
34
46
|
query: z.string().optional().default("").describe("Search query string; optional when browsing a subforum"),
|
|
35
47
|
subforum: z.string().optional().describe("Subforum slug to browse directly (e.g. 'apex-legends')"),
|
|
@@ -37,7 +49,8 @@ export function registerSearchForum(server: McpServer): void {
|
|
|
37
49
|
sort_by: z.enum(["relevancy", "lastpost", "replycount", "views", "threadstart"]).optional().default("relevancy").describe("Sort results by"),
|
|
38
50
|
search_user: z.string().optional().describe("Filter by thread author username"),
|
|
39
51
|
},
|
|
40
|
-
|
|
52
|
+
{ readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
53
|
+
async ({ query, subforum, title_only, sort_by, search_user }) => withBrowserSession(async () => {
|
|
41
54
|
try {
|
|
42
55
|
if (!subforum && !query.trim()) {
|
|
43
56
|
return {
|
|
@@ -49,7 +62,9 @@ export function registerSearchForum(server: McpServer): void {
|
|
|
49
62
|
if (subforum) {
|
|
50
63
|
const url = `https://www.unknowncheats.me/forum/${subforum}/`;
|
|
51
64
|
const html = await fetchHtml(url);
|
|
52
|
-
const
|
|
65
|
+
const entries = parseThreadList(html);
|
|
66
|
+
if (entries.length > 0) getForumIndex().recordListing(subforum, 1, entries);
|
|
67
|
+
const results = filterThreads(entries, { query, includeSticky: true });
|
|
53
68
|
return {
|
|
54
69
|
content: [{ type: "text", text: JSON.stringify({ count: results.length, subforum, results }) }],
|
|
55
70
|
};
|
|
@@ -107,6 +122,7 @@ export function registerSearchForum(server: McpServer): void {
|
|
|
107
122
|
|
|
108
123
|
const html = await page.content();
|
|
109
124
|
const results = parseThreadList(html);
|
|
125
|
+
if (results.length > 0) getForumIndex().recordSearchResults(results);
|
|
110
126
|
const $ = load(html);
|
|
111
127
|
const pageTitle = $("title").text().trim();
|
|
112
128
|
|
|
@@ -141,6 +157,6 @@ export function registerSearchForum(server: McpServer): void {
|
|
|
141
157
|
};
|
|
142
158
|
}
|
|
143
159
|
}
|
|
144
|
-
}
|
|
160
|
+
})
|
|
145
161
|
);
|
|
146
162
|
}
|