foolfuuka-mcp-server 0.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/README.md ADDED
@@ -0,0 +1,127 @@
1
+ # foolfuuka-mcp-server
2
+
3
+ MCP server for querying 4chan archives (Desuarchive, 4plebs, b4k, archived.moe) via the [FoolFuuka](https://github.com/FoolCode/FoolFuuka) API. Enables AI agents to search and retrieve historical archived posts through the [Model Context Protocol](https://modelcontextprotocol.io).
4
+
5
+ ## Quick start
6
+
7
+ ```sh
8
+ # Run directly (no install needed)
9
+ npx foolfuuka-mcp-server
10
+
11
+ # Or install globally
12
+ npm install -g foolfuuka-mcp-server
13
+ foolfuuka-mcp-server
14
+ ```
15
+
16
+ Add to your MCP client config (Claude Desktop, VS Code, etc.):
17
+
18
+ ```json
19
+ {
20
+ "mcpServers": {
21
+ "foolfuuka": {
22
+ "command": "npx",
23
+ "args": ["foolfuuka-mcp-server"]
24
+ }
25
+ }
26
+ }
27
+ ```
28
+
29
+ ## Configuration
30
+
31
+ | Environment variable | Default | Description |
32
+ |---|---|---|
33
+ | `FOOLFUUKA_BASE_URL` | `https://desuarchive.org` | Root URL of a FoolFuuka archive |
34
+ | `FOOLFUUKA_USER_AGENT` | `foolfuuka-mcp-server/1.0` | User-Agent header sent with API requests |
35
+
36
+ > [!TIP]
37
+ > Desuarchive works out of the box. For Cloudflare-protected archives (4plebs, archived.moe), you may need to run the server on a machine with the archive whitelisted, or use a different base URL.
38
+
39
+ ## Tools
40
+
41
+ ### `search_archive`
42
+
43
+ Full-text search across archived posts with filters.
44
+
45
+ **Parameters:** `text`, `boards`, `subject`, `username`, `tripcode`, `capcode`, `filename`, `image`, `uid`, `country`, `deleted`, `ghost`, `filter`, `type`, `start`, `end`, `results`, `order`, `page`
46
+
47
+ Returns a markdown table of matching posts with excerpts.
48
+
49
+ ### `get_thread`
50
+
51
+ Retrieve all posts in a thread.
52
+
53
+ **Parameters:** `board`, `num`, `latest_doc_id` (incremental), `last_limit` (default 100)
54
+
55
+ Returns the OP post and all replies in markdown with author, timestamp, content, and media.
56
+
57
+ ### `get_post`
58
+
59
+ Retrieve a single post.
60
+
61
+ **Parameters:** `board`, `num` (supports `_` suffix for ghost posts)
62
+
63
+ Returns full post details including media link when present.
64
+
65
+ ### `list_boards`
66
+
67
+ List available boards for the configured archive.
68
+
69
+ **Parameters:** none
70
+
71
+ Returns a markdown table of board shortnames and names.
72
+
73
+ ## Examples
74
+
75
+ ```json
76
+ {
77
+ "jsonrpc": "2.0",
78
+ "id": 1,
79
+ "method": "tools/call",
80
+ "params": {
81
+ "name": "search_archive",
82
+ "arguments": { "boards": "a", "text": "hello world", "page": 1 }
83
+ }
84
+ }
85
+ ```
86
+
87
+ ```json
88
+ {
89
+ "jsonrpc": "2.0",
90
+ "id": 2,
91
+ "method": "tools/call",
92
+ "params": {
93
+ "name": "get_thread",
94
+ "arguments": { "board": "a", "num": 112800651 }
95
+ }
96
+ }
97
+ ```
98
+
99
+ ## How it works
100
+
101
+ The server runs over **stdio transport** — it reads JSON-RPC messages from stdin and writes responses to stdout. There is no HTTP server.
102
+
103
+ ```
104
+ LLM <--MCP stdio--> foolfuuka-mcp-server <--HTTP--> FoolFuuka Archive
105
+ ```
106
+
107
+ All responses are formatted as markdown rather than raw JSON, making them natural for LLMs to consume without extra parsing.
108
+
109
+ ## Rate limits
110
+
111
+ FoolFuuka archives enforce rate limits (e.g., ~5 requests/minute on 4plebs). The server respects `Retry-After` headers on 429 responses and surfaces the wait time to the caller.
112
+
113
+ ## Development
114
+
115
+ ```sh
116
+ git clone https://github.com/dynmie/foolfuuka-mcp-server.git
117
+ cd foolfuuka-mcp-server
118
+ npm install
119
+ npm run build
120
+ npm test
121
+ ```
122
+
123
+ Tests are written with [Vitest](https://vitest.dev) and live in `tests/`.
124
+
125
+ ## License
126
+
127
+ MIT
@@ -0,0 +1,31 @@
1
+ import type { Post, SearchParams } from "./types.js";
2
+ export declare function searchPosts(params: SearchParams): Promise<{
3
+ posts: Post[];
4
+ meta?: {
5
+ total_found: number;
6
+ max_results: number;
7
+ };
8
+ }>;
9
+ export declare function getThread(board: string, num: number, latest_doc_id?: number, last_limit?: number): Promise<{
10
+ op: Post;
11
+ posts: Record<string, Post>;
12
+ }>;
13
+ export declare function getPost(board: string, num: string): Promise<Post>;
14
+ export declare function listBoards(): Promise<{
15
+ site: {
16
+ url: string;
17
+ name: string;
18
+ title: string;
19
+ global_search_enabled: boolean;
20
+ };
21
+ boards: Array<{
22
+ shortname: string;
23
+ name: string;
24
+ search_enabled: boolean;
25
+ }>;
26
+ }>;
27
+ export declare function parseGhostNum(num: string): {
28
+ postNum: string;
29
+ subnum: string;
30
+ };
31
+ //# sourceMappingURL=foolfuuka.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"foolfuuka.d.ts","sourceRoot":"","sources":["../src/foolfuuka.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAIV,IAAI,EACJ,YAAY,EACb,MAAM,YAAY,CAAC;AA0DpB,wBAAsB,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IAAC,IAAI,CAAC,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC,CAmBvI;AAED,wBAAsB,SAAS,CAC7B,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,EACX,aAAa,CAAC,EAAE,MAAM,EACtB,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;CAAE,CAAC,CAWpD;AAED,wBAAsB,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAIvE;AAgFD,wBAAsB,UAAU,IAAI,OAAO,CAAC;IAC1C,IAAI,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,qBAAqB,EAAE,OAAO,CAAA;KAAE,CAAC;IACnF,MAAM,EAAE,KAAK,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CAC7E,CAAC,CAiBD;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAG9E"}
@@ -0,0 +1,179 @@
1
+ function getBaseUrl() {
2
+ return process.env.FOOLFUUKA_BASE_URL || "https://desuarchive.org";
3
+ }
4
+ function getUserAgent() {
5
+ return process.env.FOOLFUUKA_USER_AGENT || "foolfuuka-mcp-server/1.0";
6
+ }
7
+ function buildUrl(path, params) {
8
+ const base = getBaseUrl().replace(/\/+$/, "");
9
+ const url = new URL(`${base}/_/api/chan${path}`);
10
+ if (params) {
11
+ for (const [key, value] of Object.entries(params)) {
12
+ if (value !== undefined && value !== null) {
13
+ url.searchParams.set(key, String(value));
14
+ }
15
+ }
16
+ }
17
+ return url.toString();
18
+ }
19
+ async function apiFetch(url) {
20
+ const res = await fetch(url, {
21
+ headers: { "User-Agent": getUserAgent() },
22
+ });
23
+ if (res.status === 429) {
24
+ const retryAfter = res.headers.get("Retry-After");
25
+ const msg = retryAfter
26
+ ? `Rate limited. Retry after ${retryAfter} seconds.`
27
+ : "Rate limited. Try again later.";
28
+ throw new Error(msg);
29
+ }
30
+ if (res.status === 204) {
31
+ return null;
32
+ }
33
+ if (!res.ok) {
34
+ let detail = res.statusText;
35
+ try {
36
+ const errBody = await res.json();
37
+ if (errBody?.error)
38
+ detail = String(errBody.error);
39
+ }
40
+ catch { /* ignore parse failures */ }
41
+ throw new Error(`HTTP ${res.status}: ${detail}`);
42
+ }
43
+ const body = await res.json();
44
+ if (body && typeof body === "object" && "error" in body) {
45
+ throw new Error(String(body.error));
46
+ }
47
+ return body;
48
+ }
49
+ export async function searchPosts(params) {
50
+ const queryParams = {};
51
+ for (const [key, value] of Object.entries(params)) {
52
+ if (value !== undefined) {
53
+ queryParams[key] = value;
54
+ }
55
+ }
56
+ const url = buildUrl("/search/", queryParams);
57
+ const body = await apiFetch(url);
58
+ if (!body)
59
+ return { posts: [], meta: { total_found: 0, max_results: 0 } };
60
+ const posts = body["0"]?.posts ?? [];
61
+ const meta = body.meta
62
+ ? { total_found: body.meta.total_found, max_results: body.meta.max_results }
63
+ : undefined;
64
+ return { posts, meta };
65
+ }
66
+ export async function getThread(board, num, latest_doc_id, last_limit) {
67
+ const url = buildUrl("/thread/", { board, num, latest_doc_id, last_limit });
68
+ const body = await apiFetch(url);
69
+ if (!body)
70
+ return { op: {}, posts: {} };
71
+ const threadKey = String(num);
72
+ const thread = body[threadKey];
73
+ if (!thread)
74
+ throw new Error("Thread not found.");
75
+ return { op: thread.op, posts: thread.posts };
76
+ }
77
+ export async function getPost(board, num) {
78
+ const url = buildUrl("/post/", { board, num });
79
+ const body = await apiFetch(url);
80
+ return body;
81
+ }
82
+ const FOURCHAN_BOARDS = [
83
+ { shortname: "a", name: "Anime & Manga" },
84
+ { shortname: "b", name: "Random" },
85
+ { shortname: "c", name: "Anime/Cute" },
86
+ { shortname: "d", name: "Hentai/Alternative" },
87
+ { shortname: "e", name: "Mecha" },
88
+ { shortname: "f", name: "Flash" },
89
+ { shortname: "g", name: "Technology" },
90
+ { shortname: "gif", name: "GIF" },
91
+ { shortname: "h", name: "Hentai" },
92
+ { shortname: "hr", name: "High Resolution" },
93
+ { shortname: "k", name: "Weapons" },
94
+ { shortname: "m", name: "Mecha" },
95
+ { shortname: "o", name: "Auto" },
96
+ { shortname: "p", name: "Photography" },
97
+ { shortname: "s", name: "Sexy Beautiful Women" },
98
+ { shortname: "t", name: "Torrents" },
99
+ { shortname: "u", name: "Yuri" },
100
+ { shortname: "v", name: "Video Games" },
101
+ { shortname: "vg", name: "Video Game Generals" },
102
+ { shortname: "vm", name: "Video Games/Mecha" },
103
+ { shortname: "vmg", name: "Video Games/Mobile" },
104
+ { shortname: "vr", name: "Retro Games" },
105
+ { shortname: "vrpg", name: "Video Games/RPG" },
106
+ { shortname: "vst", name: "Video Games/Strategy" },
107
+ { shortname: "w", name: "Wallpapers" },
108
+ { shortname: "wg", name: "Wallpapers/General" },
109
+ { shortname: "i", name: "Oekaki" },
110
+ { shortname: "ic", name: "Artwork/Critique" },
111
+ { shortname: "r9k", name: "ROBOT9001" },
112
+ { shortname: "s4s", name: "Shit 4chan Says" },
113
+ { shortname: "vip", name: "Very Important Posts" },
114
+ { shortname: "cm", name: "Cute/Male" },
115
+ { shortname: "hm", name: "Handsome Men" },
116
+ { shortname: "lgbt", name: "LGBT" },
117
+ { shortname: "y", name: "Yaoi" },
118
+ { shortname: "3", name: "3DCG" },
119
+ { shortname: "aco", name: "Adult Cartoons" },
120
+ { shortname: "adv", name: "Advice" },
121
+ { shortname: "an", name: "Animals & Nature" },
122
+ { shortname: "bant", name: "Banned" },
123
+ { shortname: "biz", name: "Business & Finance" },
124
+ { shortname: "cgl", name: "Cosplay & EGL" },
125
+ { shortname: "ck", name: "Food & Cooking" },
126
+ { shortname: "co", name: "Comics & Cartoons" },
127
+ { shortname: "diy", name: "Do It Yourself" },
128
+ { shortname: "fa", name: "Fashion" },
129
+ { shortname: "fit", name: "Fitness" },
130
+ { shortname: "gd", name: "Graphic Design" },
131
+ { shortname: "hc", name: "Hardcore" },
132
+ { shortname: "his", name: "History & Humanities" },
133
+ { shortname: "int", name: "International" },
134
+ { shortname: "jp", name: "Otaku Culture" },
135
+ { shortname: "lit", name: "Literature" },
136
+ { shortname: "mlp", name: "My Little Pony" },
137
+ { shortname: "mu", name: "Music" },
138
+ { shortname: "n", name: "Transportation" },
139
+ { shortname: "news", name: "Current News" },
140
+ { shortname: "out", name: "Outdoors" },
141
+ { shortname: "po", name: "Papercraft & Origami" },
142
+ { shortname: "pol", name: "Politically Incorrect" },
143
+ { shortname: "pw", name: "Professional Wrestling" },
144
+ { shortname: "qst", name: "Quest" },
145
+ { shortname: "sci", name: "Science & Math" },
146
+ { shortname: "soc", name: "Social" },
147
+ { shortname: "sp", name: "Sports" },
148
+ { shortname: "tg", name: "Traditional Games" },
149
+ { shortname: "toy", name: "Toys" },
150
+ { shortname: "trv", name: "Travel" },
151
+ { shortname: "tv", name: "Television & Film" },
152
+ { shortname: "vp", name: "Veteran Posts" },
153
+ { shortname: "vt", name: "Virtual YouTubers" },
154
+ { shortname: "wsg", name: "Worksafe GIF" },
155
+ { shortname: "wsr", name: "Worksafe Requests" },
156
+ { shortname: "x", name: "Paranormal" },
157
+ { shortname: "xs", name: "Extreme Sports" },
158
+ ];
159
+ export async function listBoards() {
160
+ let site;
161
+ try {
162
+ const url = buildUrl("/boards/");
163
+ const body = await apiFetch(url);
164
+ site = body.site ?? { url: getBaseUrl(), name: "4chan", title: "4chan", global_search_enabled: true };
165
+ }
166
+ catch {
167
+ site = { url: getBaseUrl(), name: "4chan", title: "4chan", global_search_enabled: true };
168
+ }
169
+ const boards = FOURCHAN_BOARDS.map((b) => ({
170
+ ...b,
171
+ search_enabled: true,
172
+ }));
173
+ return { site, boards };
174
+ }
175
+ export function parseGhostNum(num) {
176
+ const parts = num.split("_");
177
+ return { postNum: parts[0], subnum: parts[1] || "0" };
178
+ }
179
+ //# sourceMappingURL=foolfuuka.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"foolfuuka.js","sourceRoot":"","sources":["../src/foolfuuka.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import type { Post, Media } from "./types.js";
2
+ export declare function formatSuccess(text: string): {
3
+ content: {
4
+ type: "text";
5
+ text: string;
6
+ }[];
7
+ };
8
+ export declare function formatError(text: string): {
9
+ content: {
10
+ type: "text";
11
+ text: string;
12
+ }[];
13
+ };
14
+ export declare function formatHumanSize(bytes: string | null): string;
15
+ export declare function sanitizeTableCell(text: string, maxLen?: number): string;
16
+ export declare function formatPostContent(comment: string | null): string;
17
+ export declare function formatMediaLine(media: Media | null, includeUrl?: boolean): string;
18
+ export declare function formatTimestamp(ts: number | string, short?: boolean): string;
19
+ export declare function formatAuthorLine(name: string, trip: string | null, capcode: string): string;
20
+ export declare function formatPostMeta(post: Post): string[];
21
+ //# sourceMappingURL=format.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAE9C,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM;;;;;EAEzC;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM;;;;;EAEvC;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAO5D;AAOD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,SAAM,GAAG,MAAM,CAMpE;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAMhE;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,EAAE,UAAU,UAAQ,GAAG,MAAM,CAU/E;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,EAAE,KAAK,UAAQ,GAAG,MAAM,CAY1E;AASD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAM3F;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,EAAE,CAInD"}
package/dist/format.js ADDED
@@ -0,0 +1,87 @@
1
+ export function formatSuccess(text) {
2
+ return { content: [{ type: "text", text }] };
3
+ }
4
+ export function formatError(text) {
5
+ return { content: [{ type: "text", text: `**Error:** ${text}` }] };
6
+ }
7
+ export function formatHumanSize(bytes) {
8
+ if (!bytes)
9
+ return "";
10
+ const n = parseInt(bytes, 10);
11
+ if (isNaN(n))
12
+ return bytes;
13
+ if (n < 1024)
14
+ return `${n} B`;
15
+ if (n < 1048576)
16
+ return `${Math.round(n / 1024)} KB`;
17
+ return `${(n / 1048576).toFixed(1)} MB`;
18
+ }
19
+ function formatDimensions(w, h) {
20
+ if (!w || !h)
21
+ return "";
22
+ return `${parseInt(w, 10)}×${parseInt(h, 10)}`;
23
+ }
24
+ export function sanitizeTableCell(text, maxLen = 120) {
25
+ const withoutNewlines = text.replace(/\s+/g, " ").trim();
26
+ const truncated = withoutNewlines.length > maxLen
27
+ ? withoutNewlines.slice(0, maxLen) + "..."
28
+ : withoutNewlines;
29
+ return truncated.replace(/\|/g, "\\|");
30
+ }
31
+ export function formatPostContent(comment) {
32
+ if (!comment)
33
+ return "> *[no content]*";
34
+ return comment
35
+ .split("\n")
36
+ .map((line) => `> ${line}`)
37
+ .join("\n");
38
+ }
39
+ export function formatMediaLine(media, includeUrl = false) {
40
+ if (!media || !media.media_filename)
41
+ return "";
42
+ const filename = media.media_filename;
43
+ const dims = formatDimensions(media.media_w, media.media_h);
44
+ const size = formatHumanSize(media.media_size);
45
+ let line = `📎 ${filename} (${dims}${size ? `, ${size}` : ""})`;
46
+ if (includeUrl && media.media_link) {
47
+ line += `\n[View image](${media.media_link})`;
48
+ }
49
+ return line;
50
+ }
51
+ export function formatTimestamp(ts, short = false) {
52
+ const n = typeof ts === "string" ? parseInt(ts, 10) : ts;
53
+ if (isNaN(n))
54
+ return String(ts);
55
+ const d = new Date(n * 1000);
56
+ const y = d.getUTCFullYear();
57
+ const mo = String(d.getUTCMonth() + 1).padStart(2, "0");
58
+ const day = String(d.getUTCDate()).padStart(2, "0");
59
+ if (short)
60
+ return `${y}-${mo}-${day}`;
61
+ const h = String(d.getUTCHours()).padStart(2, "0");
62
+ const mi = String(d.getUTCMinutes()).padStart(2, "0");
63
+ const s = String(d.getUTCSeconds()).padStart(2, "0");
64
+ return `${y}-${mo}-${day} ${h}:${mi}:${s} UTC`;
65
+ }
66
+ const capcodeLabels = {
67
+ M: "Mod",
68
+ A: "Admin",
69
+ D: "Developer",
70
+ V: "Verified",
71
+ };
72
+ export function formatAuthorLine(name, trip, capcode) {
73
+ let result = name;
74
+ if (trip)
75
+ result += ` !!${trip}`;
76
+ const label = capcodeLabels[capcode];
77
+ if (label)
78
+ result += ` (${label})`;
79
+ return result;
80
+ }
81
+ export function formatPostMeta(post) {
82
+ const lines = [];
83
+ if (post.title)
84
+ lines.push(`**Subject:** ${post.title}`);
85
+ return lines;
86
+ }
87
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.js","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
package/dist/index.js ADDED
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import { searchPosts, getThread, getPost, listBoards, } from "./foolfuuka.js";
6
+ import { formatSuccess, formatError, sanitizeTableCell, formatPostContent, formatMediaLine, formatTimestamp, formatAuthorLine, formatPostMeta, } from "./format.js";
7
+ const server = new McpServer({ name: "foolfuuka-mcp-server", version: "0.1.0" }, { capabilities: { tools: {} } });
8
+ server.registerTool("search_archive", {
9
+ description: "Full-text search across archived 4chan posts. Filter by board, text, date range, file type, and more. Returns matching posts with excerpts and metadata.",
10
+ inputSchema: {
11
+ text: z.string().optional().describe("Comment text to search for"),
12
+ boards: z.string().optional().describe("Dot-delimited board shortnames, e.g. \"a\" or \"adv.trv\". Omit to search all boards"),
13
+ subject: z.string().optional().describe("OP subject line search"),
14
+ username: z.string().optional().describe("Poster name match"),
15
+ tripcode: z.string().optional().describe("Tripcode match"),
16
+ capcode: z.string().optional().describe("One of: user, mod, admin, dev, manager, founder"),
17
+ filename: z.string().optional().describe("Original filename search"),
18
+ image: z.string().optional().describe("Base64 MD5 hash of media"),
19
+ uid: z.string().optional().describe("4chan Pass UID"),
20
+ country: z.string().optional().describe("2-letter ISO 3166 country code"),
21
+ deleted: z.string().optional().describe("deleted or not-deleted"),
22
+ ghost: z.string().optional().describe("only or none"),
23
+ filter: z.string().optional().describe("image or text"),
24
+ type: z.string().optional().describe("sticky, op, or posts"),
25
+ start: z.string().optional().describe("Start date in YYYY-MM-DD format"),
26
+ end: z.string().optional().describe("End date in YYYY-MM-DD format"),
27
+ results: z.string().optional().describe("thread to group by thread, otherwise flat"),
28
+ order: z.string().default("desc").describe("asc or desc"),
29
+ page: z.number().default(1).describe("Page number (1-indexed, 25 posts per page)"),
30
+ },
31
+ }, async (args) => {
32
+ try {
33
+ const result = await searchPosts(args);
34
+ if (result.posts.length === 0) {
35
+ return formatSuccess(`No posts found matching "${args.text ?? ""}".`);
36
+ }
37
+ const isMultiBoard = !args.boards || args.boards.includes(".");
38
+ const totalFound = result.meta?.total_found ?? result.posts.length;
39
+ const maxResults = result.meta?.max_results ?? 0;
40
+ const totalPages = maxResults > 0 ? Math.ceil(maxResults / 25) : 1;
41
+ let md = `## Search: "${args.text ?? ""}"${args.boards ? ` on /${args.boards}/` : " (all boards)"}\n\n`;
42
+ md += `Found ${result.posts.length} of ${totalFound.toLocaleString()} matching posts (page ${args.page} of ${totalPages}, 25 per page)\n\n`;
43
+ if (isMultiBoard) {
44
+ md += "| Board | Post | Date | Author | Excerpt |\n|-------|------|------|--------|---------|\n";
45
+ }
46
+ else {
47
+ md += "| Post | Date | Author | Excerpt |\n|------|------|--------|---------|\n";
48
+ }
49
+ for (const post of result.posts) {
50
+ const boardShort = post.board?.shortname ? `/${post.board.shortname}/` : "";
51
+ const excerpt = sanitizeTableCell(post.comment_sanitized || "");
52
+ const date = formatTimestamp(post.timestamp, true);
53
+ const author = sanitizeTableCell(post.name, 40);
54
+ if (isMultiBoard) {
55
+ md += `| ${boardShort} | #${post.num} | ${date} | ${author} | ${excerpt} |\n`;
56
+ }
57
+ else {
58
+ md += `| #${post.num} | ${date} | ${author} | ${excerpt} |\n`;
59
+ }
60
+ }
61
+ md += `\n*Use \`get_thread\` to view a full thread or \`get_post\` for a single post.*`;
62
+ return formatSuccess(md);
63
+ }
64
+ catch (err) {
65
+ return formatError(err instanceof Error ? err.message : String(err));
66
+ }
67
+ });
68
+ server.registerTool("get_thread", {
69
+ description: "Retrieve all posts in a thread from a 4chan archive. Returns the OP post and all replies with full content, timestamps, and media attachments.",
70
+ inputSchema: {
71
+ board: z.string().describe("Board shortname, e.g. \"a\""),
72
+ num: z.number().describe("Thread OP post number"),
73
+ latest_doc_id: z.number().optional().describe("For incremental fetch; returns only posts after this internal doc_id"),
74
+ last_limit: z.number().default(100).describe("Only return the last N posts. Default 100. Pass 0 for all."),
75
+ },
76
+ }, async (args) => {
77
+ try {
78
+ const { op, posts } = await getThread(args.board, args.num, args.latest_doc_id, args.last_limit);
79
+ const postEntries = Object.entries(posts);
80
+ let md = `## Thread #${args.num} on /${args.board}/\n\n`;
81
+ if (args.latest_doc_id) {
82
+ md += `Showing ${postEntries.length} new posts since doc_id ${args.latest_doc_id}\n\n`;
83
+ }
84
+ else if (args.last_limit > 0) {
85
+ md += `Showing last ${Math.min(args.last_limit, postEntries.length)} of ${postEntries.length} posts\n\n`;
86
+ }
87
+ const postToMd = (post, isOp) => {
88
+ const ghost = post.subnum !== "0" ? " (ghost)" : "";
89
+ const lineStart = isOp ? "**OP**" : `**#${post.num}**${ghost}`;
90
+ md += `${lineStart} by ${formatAuthorLine(post.name, post.trip, post.capcode)} — ${formatTimestamp(post.timestamp)}\n`;
91
+ for (const meta of formatPostMeta(post)) {
92
+ md += `${meta}\n`;
93
+ }
94
+ md += `${formatPostContent(post.comment_sanitized)}\n`;
95
+ const mediaLine = formatMediaLine(post.media, false);
96
+ if (mediaLine)
97
+ md += `${mediaLine}\n`;
98
+ };
99
+ postToMd(op, true);
100
+ md += `\n---\n\n`;
101
+ for (const [, post] of postEntries) {
102
+ postToMd(post, false);
103
+ md += `\n---\n\n`;
104
+ }
105
+ return formatSuccess(md.trim());
106
+ }
107
+ catch (err) {
108
+ return formatError(err instanceof Error ? err.message : String(err));
109
+ }
110
+ });
111
+ server.registerTool("get_post", {
112
+ description: "Retrieve a single post from a 4chan archive by board and post number. Includes post content, author info, timestamps, and media if present.",
113
+ inputSchema: {
114
+ board: z.string().describe("Board shortname, e.g. \"a\""),
115
+ num: z.string().describe("Post number. Can include _ suffix for ghost posts, e.g. \"676_1\""),
116
+ },
117
+ }, async (args) => {
118
+ try {
119
+ const post = await getPost(args.board, args.num);
120
+ const ghost = post.subnum !== "0" ? " (ghost)" : "";
121
+ let md = `## Post #${args.num}${ghost} on /${args.board}/\n\n`;
122
+ md += `**Author:** ${formatAuthorLine(post.name, post.trip, post.capcode)} | **Date:** ${formatTimestamp(post.timestamp)}\n`;
123
+ md += `**Board:** /${args.board}/ | **Thread:** #${post.thread_num}\n`;
124
+ for (const meta of formatPostMeta(post)) {
125
+ md += `${meta}\n`;
126
+ }
127
+ md += `\n${formatPostContent(post.comment_sanitized)}\n`;
128
+ const mediaLine = formatMediaLine(post.media, true);
129
+ if (mediaLine)
130
+ md += `\n${mediaLine}\n`;
131
+ return formatSuccess(md.trim());
132
+ }
133
+ catch (err) {
134
+ return formatError(err instanceof Error ? err.message : String(err));
135
+ }
136
+ });
137
+ server.registerTool("list_boards", {
138
+ description: "List all available boards on the configured 4chan archive. Returns board shortnames and full names, plus site metadata.",
139
+ inputSchema: {},
140
+ }, async () => {
141
+ try {
142
+ const { site, boards } = await listBoards();
143
+ let md = `## ${site.name}\n\nAvailable boards:\n\n`;
144
+ md += "| Board | Name |\n|-------|------|\n";
145
+ for (const b of boards) {
146
+ md += `| /${b.shortname}/ | ${b.name} |\n`;
147
+ }
148
+ const searchEnabled = boards.filter((b) => b.search_enabled).length;
149
+ md += `\n*Search enabled on ${searchEnabled} of ${boards.length} boards.*`;
150
+ return formatSuccess(md);
151
+ }
152
+ catch (err) {
153
+ return formatError(err instanceof Error ? err.message : String(err));
154
+ }
155
+ });
156
+ async function main() {
157
+ const transport = new StdioServerTransport();
158
+ await server.connect(transport);
159
+ }
160
+ main().catch((err) => {
161
+ console.error(err);
162
+ process.exit(1);
163
+ });
164
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,123 @@
1
+ export interface Media {
2
+ media_id: string;
3
+ spoiler: "0" | "1";
4
+ preview_orig: string | null;
5
+ media: string | null;
6
+ preview_op: string | null;
7
+ preview_reply: string | null;
8
+ preview_w: string | null;
9
+ preview_h: string | null;
10
+ media_filename: string | null;
11
+ media_w: string | null;
12
+ media_h: string | null;
13
+ media_size: string | null;
14
+ media_hash: string | null;
15
+ media_orig: string | null;
16
+ total: string;
17
+ banned: "0" | "1";
18
+ media_status: string;
19
+ remote_media_link: string | null;
20
+ media_link: string | null;
21
+ thumb_link: string | null;
22
+ media_filename_processed: string | null;
23
+ }
24
+ export interface Post {
25
+ doc_id: string;
26
+ num: string;
27
+ subnum: string;
28
+ thread_num: string;
29
+ op: "0" | "1";
30
+ timestamp: number | string;
31
+ capcode: string;
32
+ email: string | null;
33
+ name: string;
34
+ trip: string | null;
35
+ title: string | null;
36
+ comment: string | null;
37
+ comment_sanitized: string;
38
+ comment_processed: string;
39
+ poster_hash: string | null;
40
+ poster_country: string | null;
41
+ sticky: "0" | "1";
42
+ locked: "0" | "1";
43
+ deleted: "0" | "1";
44
+ nreplies: number | string | null;
45
+ nimages: number | string | null;
46
+ fourchan_date: string;
47
+ formatted: boolean;
48
+ media: Media | null;
49
+ board?: BoardInfo;
50
+ }
51
+ export interface BoardInfo {
52
+ name: string;
53
+ shortname: string;
54
+ }
55
+ export interface Site {
56
+ url: string;
57
+ name: string;
58
+ title: string;
59
+ global_search_enabled: boolean;
60
+ }
61
+ export interface SearchMeta {
62
+ total_found: number;
63
+ max_results: number;
64
+ search_title: string;
65
+ }
66
+ export interface SearchResponse {
67
+ "0": {
68
+ posts: Post[];
69
+ };
70
+ meta?: SearchMeta;
71
+ }
72
+ export interface ThreadResponse {
73
+ [threadNum: string]: {
74
+ op: Post;
75
+ posts: {
76
+ [postNum: string]: Post;
77
+ };
78
+ };
79
+ }
80
+ export interface BoardsResponse {
81
+ site: Site;
82
+ boards: Record<string, BoardRaw>;
83
+ }
84
+ export interface BoardRaw {
85
+ name: string;
86
+ shortname: string;
87
+ board_url: string;
88
+ threads_per_page: string;
89
+ search_enabled: boolean;
90
+ is_nsfw: boolean;
91
+ }
92
+ export interface SearchParams {
93
+ text?: string;
94
+ boards?: string;
95
+ subject?: string;
96
+ username?: string;
97
+ tripcode?: string;
98
+ capcode?: string;
99
+ filename?: string;
100
+ image?: string;
101
+ uid?: string;
102
+ country?: string;
103
+ deleted?: string;
104
+ ghost?: string;
105
+ filter?: string;
106
+ type?: string;
107
+ start?: string;
108
+ end?: string;
109
+ results?: string;
110
+ order?: string;
111
+ page?: number;
112
+ }
113
+ export interface ThreadParams {
114
+ board: string;
115
+ num: number;
116
+ latest_doc_id?: number;
117
+ last_limit?: number;
118
+ }
119
+ export interface PostParams {
120
+ board: string;
121
+ num: string;
122
+ }
123
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,KAAK;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,GAAG,GAAG,GAAG,CAAC;IACnB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,GAAG,GAAG,GAAG,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,wBAAwB,EAAE,MAAM,GAAG,IAAI,CAAC;CACzC;AAED,MAAM,WAAW,IAAI;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,EAAE,EAAE,GAAG,GAAG,GAAG,CAAC;IACd,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,MAAM,EAAE,GAAG,GAAG,GAAG,CAAC;IAClB,MAAM,EAAE,GAAG,GAAG,GAAG,CAAC;IAClB,OAAO,EAAE,GAAG,GAAG,GAAG,CAAC;IACnB,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACjC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAChC,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,KAAK,CAAC,EAAE,SAAS,CAAC;CACnB;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,IAAI;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,qBAAqB,EAAE,OAAO,CAAC;CAChC;AAED,MAAM,WAAW,UAAU;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE;QAAE,KAAK,EAAE,IAAI,EAAE,CAAA;KAAE,CAAC;IACvB,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,CAAC,SAAS,EAAE,MAAM,GAAG;QACnB,EAAE,EAAE,IAAI,CAAC;QACT,KAAK,EAAE;YAAE,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;SAAE,CAAC;KACpC,CAAC;CACH;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,IAAI,CAAC;IACX,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,EAAE,MAAM,CAAC;IACzB,cAAc,EAAE,OAAO,CAAC;IACxB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "foolfuuka-mcp-server",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for querying 4chan archives via the FoolFuuka API",
5
+ "type": "module",
6
+ "bin": {
7
+ "foolfuuka-mcp-server": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "engines": {
13
+ "node": ">=18"
14
+ },
15
+ "license": "MIT",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/dynmie/foolfuuka-mcp-server.git"
19
+ },
20
+ "keywords": [
21
+ "mcp",
22
+ "4chan",
23
+ "foolfuuka",
24
+ "archive",
25
+ "desuarchive"
26
+ ],
27
+ "scripts": {
28
+ "build": "tsc",
29
+ "start": "node dist/index.js",
30
+ "test": "vitest run",
31
+ "test:watch": "vitest",
32
+ "prepublishOnly": "npm run build"
33
+ },
34
+ "dependencies": {
35
+ "@modelcontextprotocol/sdk": "^1.16.0",
36
+ "zod": "^3.24.0"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^22.0.0",
40
+ "typescript": "^5.7.0",
41
+ "vitest": "^4.1.9"
42
+ }
43
+ }