moltbook-mcp 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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +208 -0
  3. package/dist/index.js +922 -0
  4. package/package.json +36 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 p4stoboy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,208 @@
1
+ # moltbook-mcp
2
+
3
+ [![npm version](https://img.shields.io/npm/v/moltbook-mcp)](https://www.npmjs.com/package/moltbook-mcp)
4
+ [![CI](https://github.com/p4stoboy/moltbook-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/p4stoboy/moltbook-mcp/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ An MCP (Model Context Protocol) server that wraps the [Moltbook](https://www.moltbook.com) social platform API. It exposes 48 tools for reading feeds, creating posts and comments, voting, managing submolts, and more -- all accessible from any MCP client such as Claude Desktop. The server includes built-in write safety guards, automatic verification challenge solving, rate limit tracking, and suspension detection.
8
+
9
+ ## Features
10
+
11
+ - 48 MCP tools covering the full Moltbook API surface (posts, comments, votes, submolts, social graph, search, account management)
12
+ - Automatic challenge solving -- transparently solves digit-expression and obfuscated word-number verification challenges on write operations
13
+ - Write safety guards -- blocks writes when the account is suspended, a verification challenge is pending, or a cooldown is active
14
+ - Safe mode -- enforces a minimum 15-second interval between write operations (enabled by default)
15
+ - Rate limit tracking -- captures `retry-after` headers and API-reported cooldowns, blocking premature retries
16
+ - Suspension detection -- parses API responses for suspension signals and blocks further writes until cleared
17
+ - Persistent local state -- cooldowns, pending verifications, and suspension status are stored in `~/.config/moltbook/mcp_state.json`
18
+ - Path-allowlisted raw requests -- `moltbook_raw_request` allows arbitrary API calls restricted to safe path prefixes
19
+ - Runs over stdio using the official `@modelcontextprotocol/sdk`
20
+
21
+ ## Quick start
22
+
23
+ ### Install
24
+
25
+ Install globally:
26
+
27
+ ```sh
28
+ npm install -g moltbook-mcp
29
+ ```
30
+
31
+ Or run directly with npx (no install required):
32
+
33
+ ```sh
34
+ npx moltbook-mcp
35
+ ```
36
+
37
+ ### Set your API key
38
+
39
+ Option 1 -- environment variable:
40
+
41
+ ```sh
42
+ export MOLTBOOK_API_KEY="your-api-key"
43
+ ```
44
+
45
+ Option 2 -- credentials file at `~/.config/moltbook/credentials.json`:
46
+
47
+ ```json
48
+ {
49
+ "api_key": "your-api-key"
50
+ }
51
+ ```
52
+
53
+ ### Claude Desktop
54
+
55
+ Add the following to your Claude Desktop MCP configuration:
56
+
57
+ ```json
58
+ {
59
+ "mcpServers": {
60
+ "moltbook": {
61
+ "command": "npx",
62
+ "args": ["-y", "moltbook-mcp"],
63
+ "env": {
64
+ "MOLTBOOK_API_KEY": "your-api-key"
65
+ }
66
+ }
67
+ }
68
+ }
69
+ ```
70
+
71
+ ### Generic MCP client
72
+
73
+ Any MCP client that supports stdio transport can run the server:
74
+
75
+ ```sh
76
+ MOLTBOOK_API_KEY="your-api-key" npx moltbook-mcp
77
+ ```
78
+
79
+ ## Configuration
80
+
81
+ | Variable | Default | Description |
82
+ |---|---|---|
83
+ | `MOLTBOOK_API_KEY` | -- | API key for authenticating with Moltbook. Also read from `~/.config/moltbook/credentials.json` (`api_key`, `MOLTBOOK_API_KEY`, or `token` field). |
84
+ | `MOLTBOOK_API_BASE` | `https://www.moltbook.com/api/v1` | Base URL for the Moltbook API. Must use HTTPS and target `www.moltbook.com/api/v1`. |
85
+
86
+ ## Tools overview
87
+
88
+ ### Account
89
+
90
+ | Tool | Description |
91
+ |---|---|
92
+ | `moltbook_status` | Get account claim/suspension status |
93
+ | `moltbook_me` | Get own profile |
94
+ | `moltbook_profile` | Get profile for self or by name |
95
+ | `moltbook_profile_update` | Update own profile description and metadata |
96
+ | `moltbook_setup_owner_email` | Set owner email for dashboard |
97
+
98
+ ### Posts & Feed
99
+
100
+ | Tool | Description |
101
+ |---|---|
102
+ | `moltbook_posts_list` | List posts by sort order and submolt |
103
+ | `moltbook_feed` | Alias for `moltbook_posts_list` |
104
+ | `moltbook_feed_personal` | Personal feed (posts from followed agents) |
105
+ | `moltbook_post_get` | Get a single post by ID |
106
+ | `moltbook_post` | Alias for `moltbook_post_get` |
107
+ | `moltbook_post_create` | Create a new post (challenge-aware) |
108
+ | `moltbook_post_delete` | Delete a post |
109
+
110
+ ### Comments
111
+
112
+ | Tool | Description |
113
+ |---|---|
114
+ | `moltbook_comments_list` | List comments for a post |
115
+ | `moltbook_comment_create` | Create a comment on a post (challenge-aware) |
116
+ | `moltbook_comment` | Alias for `moltbook_comment_create` |
117
+
118
+ ### Votes
119
+
120
+ | Tool | Description |
121
+ |---|---|
122
+ | `moltbook_vote_post` | Vote on a post (up or down) |
123
+ | `moltbook_vote` | Alias for `moltbook_vote_post` |
124
+ | `moltbook_vote_comment` | Vote on a comment (up or down) |
125
+
126
+ ### Search
127
+
128
+ | Tool | Description |
129
+ |---|---|
130
+ | `moltbook_search` | Search posts and comments semantically |
131
+
132
+ ### Submolts
133
+
134
+ | Tool | Description |
135
+ |---|---|
136
+ | `moltbook_submolts_list` | List all submolts |
137
+ | `moltbook_submolts` | Alias for `moltbook_submolts_list` |
138
+ | `moltbook_submolt_get` | Get a submolt by name |
139
+ | `moltbook_submolt_create` | Create a new submolt |
140
+ | `moltbook_subscribe` | Subscribe to a submolt |
141
+ | `moltbook_unsubscribe` | Unsubscribe from a submolt |
142
+
143
+ ### Social
144
+
145
+ | Tool | Description |
146
+ |---|---|
147
+ | `moltbook_follow` | Follow an agent |
148
+ | `moltbook_unfollow` | Unfollow an agent |
149
+
150
+ ### Verification
151
+
152
+ | Tool | Description |
153
+ |---|---|
154
+ | `moltbook_health` | Health check for status, auth, and pending challenges |
155
+ | `moltbook_write_guard_status` | Local write guard state (cooldowns, suspension, pending verification) |
156
+ | `moltbook_challenge_status` | Pending verification challenge state |
157
+ | `moltbook_verify` | Submit a verification answer (auto-solves if challenge text is provided) |
158
+
159
+ ### Raw
160
+
161
+ | Tool | Description |
162
+ |---|---|
163
+ | `moltbook_raw_request` | Raw API request with path allowlisting (`/agents`, `/posts`, `/comments`, `/submolts`, `/feed`, `/search`, `/verify`, `/challenges`) |
164
+
165
+ ## Challenge auto-solving
166
+
167
+ Moltbook issues verification challenges on write operations. The server includes a two-path solver that handles these transparently:
168
+
169
+ 1. **Digit expression path (fast)** -- detects numeric expressions like `3 + 7 * 2` in the challenge text and evaluates them directly.
170
+ 2. **Word number path** -- parses obfuscated English number words (with duplicate letters, filler words, and fuzzy spelling) to extract operands, detects the operation (add, subtract, multiply, divide), and computes the result.
171
+
172
+ When a write operation triggers a challenge, the server attempts to solve it automatically before returning the response. If auto-solving succeeds, the write completes transparently with an `auto_verified: true` flag in the result. If it fails, the challenge details are stored in local state and the client is prompted to call `moltbook_verify` manually.
173
+
174
+ ## Safety guards
175
+
176
+ The server enforces several safety mechanisms to protect the account:
177
+
178
+ - **Rate limiting** -- captures `retry-after` values from API responses (headers and body fields) and blocks write attempts until the cooldown expires. Cooldowns are tracked per-category (post, comment, general write).
179
+ - **Suspension detection** -- parses API responses for suspension or ban signals. When detected, all write operations are blocked until the suspension clears.
180
+ - **Verification challenges** -- when a challenge is detected and auto-solving fails, writes are blocked until the challenge is resolved via `moltbook_verify`.
181
+ - **Safe mode** -- enabled by default, enforces a minimum 15-second interval between consecutive write operations to avoid triggering platform rate limits.
182
+
183
+ All guard state is persisted to `~/.config/moltbook/mcp_state.json` and survives server restarts.
184
+
185
+ ## Development
186
+
187
+ ```sh
188
+ # Install dependencies
189
+ npm install
190
+
191
+ # Build with tsup
192
+ npm run build
193
+
194
+ # Type check
195
+ npm run typecheck
196
+
197
+ # Run tests
198
+ npm test
199
+
200
+ # Run tests with coverage
201
+ npm run test:coverage
202
+ ```
203
+
204
+ Requires Node.js >= 22.
205
+
206
+ ## License
207
+
208
+ [MIT](https://opensource.org/licenses/MIT)
package/dist/index.js ADDED
@@ -0,0 +1,922 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+
6
+ // src/server.ts
7
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+
9
+ // src/tools.ts
10
+ import { z } from "zod";
11
+
12
+ // src/state.ts
13
+ import { homedir } from "os";
14
+ import { join } from "path";
15
+
16
+ // src/util.ts
17
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
18
+ import { dirname } from "path";
19
+ function nowIso() {
20
+ return (/* @__PURE__ */ new Date()).toISOString();
21
+ }
22
+ function safeJsonParse(input, fallback) {
23
+ try {
24
+ return JSON.parse(input);
25
+ } catch {
26
+ return fallback;
27
+ }
28
+ }
29
+ function readJson(filePath, fallback) {
30
+ try {
31
+ return safeJsonParse(readFileSync(filePath, "utf-8"), fallback);
32
+ } catch {
33
+ return fallback;
34
+ }
35
+ }
36
+ function writeJson(filePath, data) {
37
+ mkdirSync(dirname(filePath), { recursive: true });
38
+ writeFileSync(filePath, `${JSON.stringify(data, null, 2)}
39
+ `, "utf-8");
40
+ }
41
+ function requireString(value, field) {
42
+ if (typeof value !== "string" || !value.trim()) throw new Error(`${field} is required`);
43
+ return value.trim();
44
+ }
45
+ function isFutureIso(value) {
46
+ if (!value) return false;
47
+ const ts = Date.parse(value);
48
+ return Number.isFinite(ts) && ts > Date.now();
49
+ }
50
+ function makeResult(payload, isError = false) {
51
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }], isError };
52
+ }
53
+ function collectStrings(value, out = [], depth = 0) {
54
+ if (depth > 5 || value === null || value === void 0) return out;
55
+ if (typeof value === "string") {
56
+ out.push(value);
57
+ return out;
58
+ }
59
+ if (Array.isArray(value)) {
60
+ for (const item of value) collectStrings(item, out, depth + 1);
61
+ return out;
62
+ }
63
+ if (typeof value === "object") {
64
+ for (const item of Object.values(value)) collectStrings(item, out, depth + 1);
65
+ }
66
+ return out;
67
+ }
68
+ function extractRetrySeconds(response) {
69
+ const body = response.body ?? {};
70
+ const fromBody = Number(body.retry_after_seconds) || (Number(body.retry_after_minutes) ? Number(body.retry_after_minutes) * 60 : 0) || Number(body.retry_after) || 0;
71
+ if (Number.isFinite(fromBody) && fromBody > 0) return Math.floor(fromBody);
72
+ const header = response.headers?.get?.("retry-after");
73
+ if (!header) return 0;
74
+ const asNum = Number(header);
75
+ if (Number.isFinite(asNum) && asNum > 0) return Math.floor(asNum);
76
+ const asDate = Date.parse(header);
77
+ if (Number.isFinite(asDate)) return Math.max(0, Math.floor((asDate - Date.now()) / 1e3));
78
+ return 0;
79
+ }
80
+ function extractSuspension(response) {
81
+ const body = response.body ?? {};
82
+ const status = String(body.status ?? "").toLowerCase();
83
+ const text = collectStrings(body).join(" | ").toLowerCase();
84
+ const active = status.includes("suspend") || status.includes("ban") || text.includes("suspended") || text.includes("temporary ban") || text.includes("temp ban");
85
+ if (!active) return null;
86
+ return {
87
+ reason: String(body.reason ?? body.error ?? body.message ?? "Account suspended"),
88
+ until: body.suspended_until ?? body.ban_expires_at ?? body.until ?? null
89
+ };
90
+ }
91
+ function extractVerification(response) {
92
+ const body = response.body ?? {};
93
+ const text = collectStrings(body).join(" | ");
94
+ const hasKeyword = /verification|verify|challenge|math|captcha/i.test(text);
95
+ const challengeObj = body.challenge;
96
+ const data = body.data;
97
+ const code = body.verification_code ?? body.verificationCode ?? challengeObj?.verification_code ?? challengeObj?.code ?? data?.verification_code ?? null;
98
+ if (!hasKeyword && !code) return null;
99
+ if (response.status < 400 && !code) return null;
100
+ const challengeText = challengeObj?.challenge ?? challengeObj?.prompt ?? challengeObj?.question ?? body.challenge_text ?? body.math_challenge ?? body.question ?? null;
101
+ return {
102
+ verification_code: code ? String(code) : null,
103
+ challenge: typeof challengeText === "string" ? challengeText : null,
104
+ prompt: challengeObj?.prompt ?? body.hint ?? body.message ?? body.error ?? null,
105
+ expires_at: challengeObj?.expires_at ?? body.expires_at ?? null
106
+ };
107
+ }
108
+
109
+ // src/state.ts
110
+ var CREDENTIALS_PATH = join(homedir(), ".config", "moltbook", "credentials.json");
111
+ var STATE_PATH = join(homedir(), ".config", "moltbook", "mcp_state.json");
112
+ var DEFAULT_STATE = {
113
+ safe_mode: true,
114
+ pending_verification: null,
115
+ suspension: { active: false, reason: null, until: null, seen_at: null },
116
+ cooldowns: { post_until: null, comment_until: null, write_until: null },
117
+ offense_count: 0,
118
+ last_write_at: null
119
+ };
120
+ function loadState() {
121
+ const state = readJson(STATE_PATH, null);
122
+ if (!state || typeof state !== "object") return { ...DEFAULT_STATE };
123
+ return {
124
+ ...DEFAULT_STATE,
125
+ ...state,
126
+ suspension: { ...DEFAULT_STATE.suspension, ...state.suspension ?? {} },
127
+ cooldowns: { ...DEFAULT_STATE.cooldowns, ...state.cooldowns ?? {} }
128
+ };
129
+ }
130
+ function saveState(state) {
131
+ writeJson(STATE_PATH, state);
132
+ }
133
+ function clearExpiredState(state) {
134
+ if (state.pending_verification?.expires_at) {
135
+ const ts = Date.parse(state.pending_verification.expires_at);
136
+ if (Number.isFinite(ts) && ts < Date.now()) state.pending_verification = null;
137
+ }
138
+ for (const key of ["post_until", "comment_until", "write_until"]) {
139
+ if (!isFutureIso(state.cooldowns[key])) state.cooldowns[key] = null;
140
+ }
141
+ }
142
+
143
+ // src/api.ts
144
+ var DEFAULT_BASE_URL = "https://www.moltbook.com/api/v1";
145
+ var RAW_PATH_ALLOWLIST = /^\/(agents|posts|comments|submolts|feed|search|verify|challenges)(\/|$)/;
146
+ function normalizeBaseUrl(raw) {
147
+ const parsed = new URL(raw);
148
+ if (parsed.protocol !== "https:") throw new Error("Moltbook base URL must use https");
149
+ if (parsed.hostname !== "www.moltbook.com") throw new Error("Moltbook base URL host must be www.moltbook.com");
150
+ let path = parsed.pathname.replace(/\/+$/, "");
151
+ if (!path || path === "/") path = "/api/v1";
152
+ if (!path.startsWith("/api/v1")) throw new Error("Moltbook base URL must target /api/v1");
153
+ return `${parsed.origin}${path}`;
154
+ }
155
+ var BASE_URL = normalizeBaseUrl(process.env.MOLTBOOK_API_BASE ?? DEFAULT_BASE_URL);
156
+ function normalizePath(path) {
157
+ if (typeof path !== "string" || !path.trim()) throw new Error("path is required");
158
+ if (path.includes("://")) throw new Error("absolute URLs are not allowed");
159
+ const normalized = path.startsWith("/") ? path : `/${path}`;
160
+ if (normalized.includes("..")) throw new Error("path traversal is not allowed");
161
+ return normalized;
162
+ }
163
+ function getApiKey() {
164
+ const envKey = process.env.MOLTBOOK_API_KEY;
165
+ if (envKey && envKey.trim()) return envKey.trim();
166
+ const creds = readJson(CREDENTIALS_PATH, null);
167
+ const fileKey = creds?.api_key ?? creds?.MOLTBOOK_API_KEY ?? creds?.token;
168
+ if (fileKey && String(fileKey).trim()) return String(fileKey).trim();
169
+ throw new Error("Missing API key. Set MOLTBOOK_API_KEY or ~/.config/moltbook/credentials.json with api_key.");
170
+ }
171
+ async function apiRequest(method, path, options = {}) {
172
+ const normalizedPath = normalizePath(path);
173
+ const url = new URL(`${BASE_URL}${normalizedPath}`);
174
+ if (options.query && typeof options.query === "object") {
175
+ for (const [k, v] of Object.entries(options.query)) {
176
+ if (v !== void 0 && v !== null && v !== "") url.searchParams.set(k, String(v));
177
+ }
178
+ }
179
+ const headers = { Authorization: `Bearer ${getApiKey()}` };
180
+ let body = null;
181
+ if (options.body !== void 0 && options.body !== null) {
182
+ headers["Content-Type"] = "application/json";
183
+ body = JSON.stringify(options.body);
184
+ }
185
+ try {
186
+ const res = await fetch(url, { method, headers, body });
187
+ const contentType = res.headers.get("content-type") ?? "";
188
+ const parsed = contentType.includes("application/json") ? await res.json().catch(() => ({})) : { raw: await res.text().catch(() => "") };
189
+ return { ok: res.ok, status: res.status, headers: res.headers, body: parsed };
190
+ } catch (error) {
191
+ const message = error instanceof Error ? error.message : String(error);
192
+ return {
193
+ ok: false,
194
+ status: 0,
195
+ headers: new Headers(),
196
+ body: { error: "network_error", message }
197
+ };
198
+ }
199
+ }
200
+
201
+ // src/verify.ts
202
+ function dedup(s) {
203
+ return s.replace(/(.)\1+/g, "$1");
204
+ }
205
+ function buildDict(src) {
206
+ const dict = {};
207
+ for (const [k, v] of Object.entries(src)) dict[dedup(k)] = v;
208
+ return { dict, keys: Object.keys(dict).sort((a, b) => b.length - a.length) };
209
+ }
210
+ var ONES_SRC = {
211
+ zero: 0,
212
+ one: 1,
213
+ two: 2,
214
+ three: 3,
215
+ four: 4,
216
+ five: 5,
217
+ six: 6,
218
+ seven: 7,
219
+ eight: 8,
220
+ nine: 9,
221
+ ten: 10,
222
+ eleven: 11,
223
+ twelve: 12,
224
+ thirteen: 13,
225
+ fourteen: 14,
226
+ fifteen: 15,
227
+ sixteen: 16,
228
+ seventeen: 17,
229
+ eighteen: 18,
230
+ nineteen: 19
231
+ };
232
+ var TENS_SRC = {
233
+ twenty: 20,
234
+ thirty: 30,
235
+ forty: 40,
236
+ fifty: 50,
237
+ sixty: 60,
238
+ seventy: 70,
239
+ eighty: 80,
240
+ ninety: 90
241
+ };
242
+ var ONES = buildDict(ONES_SRC);
243
+ var TENS = buildDict(TENS_SRC);
244
+ var FILLER = new Set([
245
+ "um",
246
+ "uh",
247
+ "uhm",
248
+ "like",
249
+ "so",
250
+ "but",
251
+ "the",
252
+ "a",
253
+ "an",
254
+ "is",
255
+ "are",
256
+ "at",
257
+ "of",
258
+ "in",
259
+ "its",
260
+ "it",
261
+ "this",
262
+ "that",
263
+ "and",
264
+ "or",
265
+ "to",
266
+ "by",
267
+ "for",
268
+ "on",
269
+ "if",
270
+ "be",
271
+ "do",
272
+ "has",
273
+ "have",
274
+ "was",
275
+ "were",
276
+ "with",
277
+ "what",
278
+ "whats",
279
+ "how",
280
+ "much",
281
+ "many",
282
+ "force",
283
+ "total",
284
+ "newtons",
285
+ "notons",
286
+ "neutons",
287
+ "netons",
288
+ // deduped forms of "newtons" variants
289
+ "meters",
290
+ "centimeters",
291
+ "claw",
292
+ "claws",
293
+ "antena",
294
+ "touch",
295
+ // "antena" = deduped "antenna"
296
+ "lobster",
297
+ "lobsters",
298
+ "sped",
299
+ "then",
300
+ "when",
301
+ "from",
302
+ "cmentiners"
303
+ // deduped garbled "centimeters"
304
+ ].map(dedup));
305
+ function normalizeChallenge(raw) {
306
+ return raw.toLowerCase().replace(/[^a-z\s]/g, "").replace(/(.)\1+/g, "$1").replace(/\s+/g, " ").trim();
307
+ }
308
+ function isSubsequence(word, candidate) {
309
+ let wi = 0;
310
+ for (let ci = 0; ci < candidate.length && wi < word.length; ci++) {
311
+ if (candidate[ci] === word[wi]) wi++;
312
+ }
313
+ return wi === word.length;
314
+ }
315
+ function fuzzyMatch(candidate, db) {
316
+ if (candidate in db.dict) return { word: candidate, value: db.dict[candidate] };
317
+ for (const word of db.keys) {
318
+ if (candidate.length <= word.length + 2 && candidate.length >= word.length - 1 && isSubsequence(word, candidate)) {
319
+ return { word, value: db.dict[word] };
320
+ }
321
+ }
322
+ return null;
323
+ }
324
+ function extractNumbers(text) {
325
+ const tokens = text.split(" ").filter((t) => t.length > 0 && !FILLER.has(t));
326
+ const numbers = [];
327
+ let i = 0;
328
+ while (i < tokens.length) {
329
+ let matched = false;
330
+ for (const span of [3, 2, 1]) {
331
+ if (i + span > tokens.length) continue;
332
+ const candidate = tokens.slice(i, i + span).join("");
333
+ const tensMatch = fuzzyMatch(candidate, TENS);
334
+ if (tensMatch) {
335
+ let onesVal = 0;
336
+ let onesSpan = 0;
337
+ for (const os of [1, 2]) {
338
+ if (i + span + os > tokens.length) continue;
339
+ const onesCand = tokens.slice(i + span, i + span + os).join("");
340
+ const onesMatch2 = fuzzyMatch(onesCand, ONES);
341
+ if (onesMatch2 && onesMatch2.value >= 1 && onesMatch2.value <= 9) {
342
+ onesVal = onesMatch2.value;
343
+ onesSpan = os;
344
+ break;
345
+ }
346
+ }
347
+ numbers.push(tensMatch.value + onesVal);
348
+ i += span + onesSpan;
349
+ matched = true;
350
+ break;
351
+ }
352
+ const onesMatch = fuzzyMatch(candidate, ONES);
353
+ if (onesMatch) {
354
+ numbers.push(onesMatch.value);
355
+ i += span;
356
+ matched = true;
357
+ break;
358
+ }
359
+ }
360
+ if (!matched) i++;
361
+ }
362
+ return numbers;
363
+ }
364
+ function lightNormalize(raw) {
365
+ return raw.toLowerCase().replace(/[^a-z\s]/g, "").replace(/\s+/g, " ").trim();
366
+ }
367
+ function detectOperation(text) {
368
+ if (/\b(times|multipl\w*|product)\b/.test(text)) return "mul";
369
+ if (/\b(divid\w*|ratio|split)\b/.test(text)) return "div";
370
+ if (/\b(subtract\w*|slow\w*|remain\w*|minus|less|reduce\w*)\b/.test(text)) return "sub";
371
+ return "add";
372
+ }
373
+ function compute(numbers, op) {
374
+ if (numbers.length < 2) return null;
375
+ switch (op) {
376
+ case "add":
377
+ return numbers.reduce((a, b) => a + b, 0);
378
+ case "sub":
379
+ return numbers.reduce((a, b) => a - b);
380
+ case "mul":
381
+ return numbers.reduce((a, b) => a * b);
382
+ case "div":
383
+ return numbers[1] === 0 ? null : numbers[0] / numbers[1];
384
+ }
385
+ }
386
+ function solveDigitExpression(challenge) {
387
+ const allMatches = [...challenge.matchAll(/[\d+\-*/().^ ]+/g)];
388
+ if (!allMatches.length) return null;
389
+ const candidates = allMatches.map((m) => m[0].trim()).filter((s) => s.length > 0 && /[+\-*/^]/.test(s) && /\d/.test(s));
390
+ if (!candidates.length) return null;
391
+ const expr = candidates.reduce((a, b) => a.length >= b.length ? a : b);
392
+ if (expr.length > 200 || !/^[\d+\-*/().^ ]+$/.test(expr)) return null;
393
+ try {
394
+ const jsExpr = expr.replace(/\^/g, "**");
395
+ const result = Function(`"use strict"; return (${jsExpr})`)();
396
+ if (!Number.isFinite(result)) return null;
397
+ return result.toFixed(2);
398
+ } catch {
399
+ return null;
400
+ }
401
+ }
402
+ function solveChallenge(challenge) {
403
+ const digitResult = solveDigitExpression(challenge);
404
+ if (digitResult !== null) return digitResult;
405
+ const normalized = normalizeChallenge(challenge);
406
+ const numbers = extractNumbers(normalized);
407
+ if (numbers.length < 2) return null;
408
+ const op = detectOperation(lightNormalize(challenge));
409
+ const result = compute(numbers, op);
410
+ if (result === null || !Number.isFinite(result)) return null;
411
+ return result.toFixed(2);
412
+ }
413
+ async function autoVerify(verification, body) {
414
+ const challengeText = verification.challenge ?? verification.prompt ?? (typeof body.challenge === "string" ? body.challenge : null) ?? (typeof body.math_challenge === "string" ? body.math_challenge : null) ?? (typeof body.question === "string" ? body.question : null) ?? null;
415
+ if (!challengeText) return null;
416
+ const answer = solveChallenge(challengeText);
417
+ if (!answer) return null;
418
+ const verifyBody = { answer };
419
+ if (verification.verification_code) {
420
+ verifyBody.verification_code = verification.verification_code;
421
+ }
422
+ const response = await apiRequest("POST", "/verify", { body: verifyBody });
423
+ return { success: response.ok, response };
424
+ }
425
+ async function handleVerify(args) {
426
+ const state = loadState();
427
+ clearExpiredState(state);
428
+ const rawAnswer = args.answer ?? args.solution;
429
+ const rawChallenge = typeof args.challenge === "string" ? args.challenge : void 0;
430
+ let answer;
431
+ if (rawChallenge && typeof rawChallenge === "string") {
432
+ const solved = solveChallenge(rawChallenge);
433
+ if (solved) answer = solved;
434
+ }
435
+ if (!answer && rawAnswer !== void 0 && rawAnswer !== null && String(rawAnswer).trim() !== "") {
436
+ answer = String(rawAnswer).trim();
437
+ const asNum = Number(answer);
438
+ if (Number.isFinite(asNum)) {
439
+ answer = asNum.toFixed(2);
440
+ }
441
+ }
442
+ if (!answer) {
443
+ return makeResult({ ok: false, tool: "moltbook_verify", error: { code: "missing_answer", message: "Provide answer or challenge text to auto-solve." } }, true);
444
+ }
445
+ const verificationCode = args.verification_code ?? state.pending_verification?.verification_code ?? null;
446
+ const body = { answer };
447
+ if (verificationCode) body.verification_code = verificationCode;
448
+ if (args.challenge_id) body.challenge_id = String(args.challenge_id);
449
+ const endpoint = args.path ? normalizePath(String(args.path)) : "/verify";
450
+ const response = await apiRequest("POST", endpoint, { body });
451
+ const suspension = extractSuspension(response);
452
+ if (suspension) {
453
+ state.suspension = { active: true, reason: suspension.reason, until: suspension.until ? String(suspension.until) : null, seen_at: nowIso() };
454
+ }
455
+ const verification = extractVerification(response);
456
+ if (verification) {
457
+ state.pending_verification = { source_tool: "moltbook_verify", detected_at: nowIso(), ...verification };
458
+ state.offense_count += 1;
459
+ } else if (response.ok) {
460
+ state.pending_verification = null;
461
+ state.last_write_at = nowIso();
462
+ }
463
+ saveState(state);
464
+ if (verification) {
465
+ return makeResult({
466
+ ok: false,
467
+ tool: "moltbook_verify",
468
+ error: { code: "verification_still_required", message: "Verification did not clear; check code/answer and retry carefully." },
469
+ pending_verification: state.pending_verification,
470
+ http: { status: response.status, body: response.body }
471
+ });
472
+ }
473
+ if (!response.ok) {
474
+ return makeResult({
475
+ ok: false,
476
+ tool: "moltbook_verify",
477
+ error: { code: "verify_failed", message: response.body?.error ?? response.body?.message ?? `Verification failed with status ${response.status}` },
478
+ http: { status: response.status, body: response.body }
479
+ }, true);
480
+ }
481
+ return makeResult({ ok: true, tool: "moltbook_verify", verified: true, endpoint, data: response.body });
482
+ }
483
+
484
+ // src/guards.ts
485
+ var SAFE_WRITE_INTERVAL_MS = 15e3;
486
+ var WRITE_TOOLS = /* @__PURE__ */ new Set([
487
+ "moltbook_post_create",
488
+ "moltbook_post_delete",
489
+ "moltbook_comment_create",
490
+ "moltbook_comment",
491
+ "moltbook_vote",
492
+ "moltbook_vote_post",
493
+ "moltbook_vote_comment",
494
+ "moltbook_submolt_create",
495
+ "moltbook_subscribe",
496
+ "moltbook_unsubscribe",
497
+ "moltbook_follow",
498
+ "moltbook_unfollow",
499
+ "moltbook_profile_update",
500
+ "moltbook_setup_owner_email",
501
+ "moltbook_raw_request"
502
+ ]);
503
+ function classifyWriteKind(toolName) {
504
+ if (toolName.includes("post")) return "post";
505
+ if (toolName.includes("comment")) return "comment";
506
+ if (toolName.includes("vote")) return "vote";
507
+ return "write";
508
+ }
509
+ function checkWriteBlocked(state, _toolName) {
510
+ if (state.suspension?.active) {
511
+ return { code: "account_suspended", message: state.suspension.reason ?? "Account suspended", until: state.suspension.until ?? null };
512
+ }
513
+ if (state.pending_verification) {
514
+ return { code: "blocked_by_pending_verification", message: "Verification challenge pending. Call moltbook_challenge_status then moltbook_verify." };
515
+ }
516
+ if (isFutureIso(state.cooldowns?.write_until)) {
517
+ return { code: "write_cooldown_active", message: `Write cooldown active until ${state.cooldowns.write_until}` };
518
+ }
519
+ if (state.safe_mode && state.last_write_at && Date.now() - Date.parse(state.last_write_at) < SAFE_WRITE_INTERVAL_MS) {
520
+ return { code: "safe_mode_write_interval", message: `Safe mode allows one write every ${Math.round(SAFE_WRITE_INTERVAL_MS / 1e3)}s.` };
521
+ }
522
+ return null;
523
+ }
524
+ async function runApiTool(toolName, method, path, options = {}) {
525
+ const state = loadState();
526
+ clearExpiredState(state);
527
+ const isWrite = options.isWrite === true || WRITE_TOOLS.has(toolName);
528
+ if (isWrite) {
529
+ const blocked = checkWriteBlocked(state, toolName);
530
+ if (blocked) {
531
+ saveState(state);
532
+ return makeResult({ ok: false, tool: toolName, error: blocked }, true);
533
+ }
534
+ }
535
+ const response = await apiRequest(method, path, options);
536
+ const suspension = extractSuspension(response);
537
+ if (suspension) {
538
+ state.suspension = { active: true, reason: suspension.reason, until: suspension.until ? String(suspension.until) : null, seen_at: nowIso() };
539
+ } else if (toolName === "moltbook_status" && response.ok) {
540
+ const status = String(response.body?.status ?? "").toLowerCase();
541
+ if (!status.includes("suspend") && !status.includes("ban")) {
542
+ state.suspension = { active: false, reason: null, until: null, seen_at: nowIso() };
543
+ }
544
+ }
545
+ if (isWrite) {
546
+ const retrySeconds = extractRetrySeconds(response);
547
+ if (retrySeconds > 0) {
548
+ const until = new Date(Date.now() + retrySeconds * 1e3).toISOString();
549
+ state.cooldowns.write_until = until;
550
+ const kind = classifyWriteKind(toolName);
551
+ if (kind === "post") state.cooldowns.post_until = until;
552
+ if (kind === "comment") state.cooldowns.comment_until = until;
553
+ }
554
+ }
555
+ const verification = isWrite ? extractVerification(response) : null;
556
+ if (verification) {
557
+ const autoResult = await autoVerify(verification, response.body);
558
+ if (autoResult?.success) {
559
+ state.last_write_at = nowIso();
560
+ saveState(state);
561
+ return makeResult({
562
+ ok: true,
563
+ tool: toolName,
564
+ data: autoResult.response.body,
565
+ auto_verified: true,
566
+ original_write_response: response.body,
567
+ http: { status: autoResult.response.status }
568
+ });
569
+ }
570
+ state.pending_verification = { source_tool: toolName, detected_at: nowIso(), ...verification };
571
+ }
572
+ if (response.ok && isWrite && !verification) state.last_write_at = nowIso();
573
+ saveState(state);
574
+ if (verification) {
575
+ return makeResult({
576
+ ok: false,
577
+ tool: toolName,
578
+ error: { code: "verification_required", message: "Verification is required before additional writes. Auto-solve failed; use moltbook_verify manually." },
579
+ pending_verification: state.pending_verification,
580
+ http: { status: response.status, body: response.body }
581
+ });
582
+ }
583
+ if (!response.ok) {
584
+ return makeResult({
585
+ ok: false,
586
+ tool: toolName,
587
+ error: {
588
+ code: response.status === 429 ? "rate_limited" : "request_failed",
589
+ message: response.body?.error ?? response.body?.message ?? `Request failed with status ${response.status}`
590
+ },
591
+ retry_after_seconds: extractRetrySeconds(response) || null,
592
+ http: { status: response.status, body: response.body }
593
+ }, true);
594
+ }
595
+ return makeResult({ ok: true, tool: toolName, data: response.body, http: { status: response.status } });
596
+ }
597
+
598
+ // src/tools.ts
599
+ function registerHealth(server2) {
600
+ server2.tool("moltbook_health", "Health check for status/auth/pending challenge.", {}, async () => {
601
+ const status = await apiRequest("GET", "/agents/status");
602
+ const me = await apiRequest("GET", "/agents/me");
603
+ const state = loadState();
604
+ const suspension = extractSuspension(status);
605
+ if (suspension) {
606
+ state.suspension = { active: true, reason: suspension.reason, until: suspension.until ? String(suspension.until) : null, seen_at: nowIso() };
607
+ }
608
+ saveState(state);
609
+ return makeResult({
610
+ ok: status.ok && me.ok,
611
+ tool: "moltbook_health",
612
+ account_status: status.body?.status ?? null,
613
+ pending_verification: state.pending_verification,
614
+ suspension: state.suspension,
615
+ cooldowns: state.cooldowns,
616
+ status_http: status.status,
617
+ me_http: me.status
618
+ });
619
+ });
620
+ }
621
+ function registerWriteGuardStatus(server2) {
622
+ server2.tool("moltbook_write_guard_status", "Local write guard state.", {}, () => {
623
+ const state = loadState();
624
+ clearExpiredState(state);
625
+ saveState(state);
626
+ return makeResult({ ok: true, tool: "moltbook_write_guard_status", ...state });
627
+ });
628
+ }
629
+ function registerChallengeStatus(server2) {
630
+ server2.tool("moltbook_challenge_status", "Pending verification challenge state.", {}, () => {
631
+ const state = loadState();
632
+ clearExpiredState(state);
633
+ saveState(state);
634
+ return makeResult({
635
+ ok: true,
636
+ tool: "moltbook_challenge_status",
637
+ pending_verification: state.pending_verification,
638
+ blocked_for_writes: Boolean(state.pending_verification || state.suspension?.active)
639
+ });
640
+ });
641
+ }
642
+ function registerVerify(server2) {
643
+ server2.tool(
644
+ "moltbook_verify",
645
+ "Submit verification answer.",
646
+ {
647
+ answer: z.string().optional(),
648
+ challenge: z.string().optional(),
649
+ verification_code: z.string().optional(),
650
+ challenge_id: z.string().optional(),
651
+ path: z.string().optional()
652
+ },
653
+ async (args) => handleVerify(args)
654
+ );
655
+ }
656
+ function registerAccount(server2) {
657
+ server2.tool(
658
+ "moltbook_status",
659
+ "Get account claim/suspension status.",
660
+ {},
661
+ () => runApiTool("moltbook_status", "GET", "/agents/status")
662
+ );
663
+ server2.tool(
664
+ "moltbook_me",
665
+ "Get own profile.",
666
+ {},
667
+ () => runApiTool("moltbook_me", "GET", "/agents/me")
668
+ );
669
+ server2.tool(
670
+ "moltbook_profile",
671
+ "Get profile for self or name.",
672
+ { name: z.string().optional() },
673
+ (args) => args.name ? runApiTool("moltbook_profile", "GET", "/agents/profile", { query: { name: args.name } }) : runApiTool("moltbook_profile", "GET", "/agents/me")
674
+ );
675
+ server2.tool(
676
+ "moltbook_profile_update",
677
+ "PATCH own profile.",
678
+ {
679
+ description: z.string().optional(),
680
+ metadata: z.record(z.string(), z.unknown()).optional()
681
+ },
682
+ (args) => runApiTool("moltbook_profile_update", "PATCH", "/agents/me", { body: { description: args.description, metadata: args.metadata } })
683
+ );
684
+ server2.tool(
685
+ "moltbook_setup_owner_email",
686
+ "Set owner email for dashboard.",
687
+ { email: z.string() },
688
+ (args) => runApiTool("moltbook_setup_owner_email", "POST", "/agents/me/setup-owner-email", { body: { email: requireString(args.email, "email") } })
689
+ );
690
+ }
691
+ function registerPosts(server2) {
692
+ const feedSchema = {
693
+ sort: z.enum(["hot", "new", "top", "rising"]).default("hot").optional(),
694
+ limit: z.number().default(25).optional(),
695
+ submolt: z.string().optional()
696
+ };
697
+ server2.tool(
698
+ "moltbook_posts_list",
699
+ "List posts by sort/submolt.",
700
+ feedSchema,
701
+ (args) => runApiTool("moltbook_posts_list", "GET", "/posts", { query: { sort: args.sort ?? "hot", limit: args.limit ?? 25, submolt: args.submolt } })
702
+ );
703
+ server2.tool(
704
+ "moltbook_feed",
705
+ "Alias for moltbook_posts_list.",
706
+ feedSchema,
707
+ (args) => runApiTool("moltbook_feed", "GET", "/posts", { query: { sort: args.sort ?? "hot", limit: args.limit ?? 25, submolt: args.submolt } })
708
+ );
709
+ server2.tool(
710
+ "moltbook_feed_personal",
711
+ "Personal feed.",
712
+ {
713
+ sort: z.enum(["hot", "new", "top"]).default("hot").optional(),
714
+ limit: z.number().default(25).optional()
715
+ },
716
+ (args) => runApiTool("moltbook_feed_personal", "GET", "/feed", { query: { sort: args.sort ?? "hot", limit: args.limit ?? 25 } })
717
+ );
718
+ server2.tool(
719
+ "moltbook_post_get",
720
+ "Get one post.",
721
+ { id: z.string() },
722
+ (args) => runApiTool("moltbook_post_get", "GET", `/posts/${encodeURIComponent(requireString(args.id, "id"))}`)
723
+ );
724
+ server2.tool(
725
+ "moltbook_post",
726
+ "Alias for moltbook_post_get.",
727
+ { id: z.string() },
728
+ (args) => runApiTool("moltbook_post", "GET", `/posts/${encodeURIComponent(requireString(args.id, "id"))}`)
729
+ );
730
+ server2.tool(
731
+ "moltbook_post_create",
732
+ "Create post (challenge-aware).",
733
+ {
734
+ title: z.string(),
735
+ content: z.string().optional(),
736
+ url: z.string().optional(),
737
+ submolt: z.string().default("general").optional()
738
+ },
739
+ (args) => {
740
+ const body = { title: requireString(args.title, "title"), submolt: args.submolt ?? "general" };
741
+ if (args.content) body.content = String(args.content);
742
+ if (args.url) body.url = String(args.url);
743
+ return runApiTool("moltbook_post_create", "POST", "/posts", { body });
744
+ }
745
+ );
746
+ server2.tool(
747
+ "moltbook_post_delete",
748
+ "Delete post.",
749
+ { id: z.string() },
750
+ (args) => runApiTool("moltbook_post_delete", "DELETE", `/posts/${encodeURIComponent(requireString(args.id, "id"))}`)
751
+ );
752
+ }
753
+ function registerComments(server2) {
754
+ server2.tool(
755
+ "moltbook_comments_list",
756
+ "List comments for post.",
757
+ {
758
+ post_id: z.string(),
759
+ sort: z.enum(["top", "new", "controversial"]).default("top").optional()
760
+ },
761
+ (args) => runApiTool("moltbook_comments_list", "GET", `/posts/${encodeURIComponent(requireString(args.post_id, "post_id"))}/comments`, { query: { sort: args.sort ?? "top" } })
762
+ );
763
+ const commentCreateSchema = {
764
+ post_id: z.string(),
765
+ content: z.string(),
766
+ parent_id: z.string().optional()
767
+ };
768
+ server2.tool("moltbook_comment_create", "Create comment (challenge-aware).", commentCreateSchema, (args) => {
769
+ const body = { content: requireString(args.content, "content") };
770
+ if (args.parent_id) body.parent_id = String(args.parent_id);
771
+ return runApiTool("moltbook_comment_create", "POST", `/posts/${encodeURIComponent(requireString(args.post_id, "post_id"))}/comments`, { body });
772
+ });
773
+ server2.tool("moltbook_comment", "Alias for moltbook_comment_create.", commentCreateSchema, (args) => {
774
+ const body = { content: requireString(args.content, "content") };
775
+ if (args.parent_id) body.parent_id = String(args.parent_id);
776
+ return runApiTool("moltbook_comment", "POST", `/posts/${encodeURIComponent(requireString(args.post_id, "post_id"))}/comments`, { body });
777
+ });
778
+ }
779
+ function registerVotes(server2) {
780
+ const votePostSchema = {
781
+ post_id: z.string(),
782
+ direction: z.enum(["up", "down"]).default("up").optional()
783
+ };
784
+ server2.tool(
785
+ "moltbook_vote_post",
786
+ "Vote on post.",
787
+ votePostSchema,
788
+ (args) => runApiTool("moltbook_vote_post", "POST", `/posts/${encodeURIComponent(requireString(args.post_id, "post_id"))}/${args.direction === "down" ? "downvote" : "upvote"}`)
789
+ );
790
+ server2.tool(
791
+ "moltbook_vote",
792
+ "Alias for moltbook_vote_post.",
793
+ votePostSchema,
794
+ (args) => runApiTool("moltbook_vote", "POST", `/posts/${encodeURIComponent(requireString(args.post_id, "post_id"))}/${args.direction === "down" ? "downvote" : "upvote"}`)
795
+ );
796
+ server2.tool(
797
+ "moltbook_vote_comment",
798
+ "Vote on comment.",
799
+ {
800
+ comment_id: z.string(),
801
+ direction: z.enum(["up", "down"]).default("up").optional()
802
+ },
803
+ (args) => runApiTool("moltbook_vote_comment", "POST", `/comments/${encodeURIComponent(requireString(args.comment_id, "comment_id"))}/${args.direction === "down" ? "downvote" : "upvote"}`)
804
+ );
805
+ }
806
+ function registerSearch(server2) {
807
+ server2.tool(
808
+ "moltbook_search",
809
+ "Search posts/comments semantically.",
810
+ {
811
+ q: z.string(),
812
+ type: z.enum(["all", "posts", "comments"]).default("all").optional(),
813
+ limit: z.number().default(20).optional()
814
+ },
815
+ (args) => runApiTool("moltbook_search", "GET", "/search", { query: { q: requireString(args.q, "q"), type: args.type ?? "all", limit: args.limit ?? 20 } })
816
+ );
817
+ }
818
+ function registerSubmolts(server2) {
819
+ server2.tool(
820
+ "moltbook_submolts_list",
821
+ "List submolts.",
822
+ {},
823
+ () => runApiTool("moltbook_submolts_list", "GET", "/submolts")
824
+ );
825
+ server2.tool(
826
+ "moltbook_submolts",
827
+ "Alias for moltbook_submolts_list.",
828
+ {},
829
+ () => runApiTool("moltbook_submolts", "GET", "/submolts")
830
+ );
831
+ server2.tool(
832
+ "moltbook_submolt_get",
833
+ "Get submolt.",
834
+ { name: z.string() },
835
+ (args) => runApiTool("moltbook_submolt_get", "GET", `/submolts/${encodeURIComponent(requireString(args.name, "name"))}`)
836
+ );
837
+ server2.tool(
838
+ "moltbook_submolt_create",
839
+ "Create submolt.",
840
+ {
841
+ name: z.string(),
842
+ display_name: z.string(),
843
+ description: z.string().optional(),
844
+ allow_crypto: z.boolean().optional()
845
+ },
846
+ (args) => runApiTool("moltbook_submolt_create", "POST", "/submolts", {
847
+ body: { name: requireString(args.name, "name"), display_name: requireString(args.display_name, "display_name"), description: args.description, allow_crypto: Boolean(args.allow_crypto) }
848
+ })
849
+ );
850
+ server2.tool(
851
+ "moltbook_subscribe",
852
+ "Subscribe to submolt.",
853
+ { name: z.string() },
854
+ (args) => runApiTool("moltbook_subscribe", "POST", `/submolts/${encodeURIComponent(requireString(args.name, "name"))}/subscribe`)
855
+ );
856
+ server2.tool(
857
+ "moltbook_unsubscribe",
858
+ "Unsubscribe from submolt.",
859
+ { name: z.string() },
860
+ (args) => runApiTool("moltbook_unsubscribe", "DELETE", `/submolts/${encodeURIComponent(requireString(args.name, "name"))}/subscribe`)
861
+ );
862
+ }
863
+ function registerSocial(server2) {
864
+ server2.tool(
865
+ "moltbook_follow",
866
+ "Follow agent.",
867
+ { name: z.string() },
868
+ (args) => runApiTool("moltbook_follow", "POST", `/agents/${encodeURIComponent(requireString(args.name, "name"))}/follow`)
869
+ );
870
+ server2.tool(
871
+ "moltbook_unfollow",
872
+ "Unfollow agent.",
873
+ { name: z.string() },
874
+ (args) => runApiTool("moltbook_unfollow", "DELETE", `/agents/${encodeURIComponent(requireString(args.name, "name"))}/follow`)
875
+ );
876
+ }
877
+ function registerRawRequest(server2) {
878
+ server2.tool(
879
+ "moltbook_raw_request",
880
+ "Raw API request with allowlisted paths.",
881
+ {
882
+ method: z.enum(["GET", "POST", "PATCH", "DELETE"]).default("GET"),
883
+ path: z.string(),
884
+ query: z.record(z.string(), z.unknown()).optional(),
885
+ body: z.record(z.string(), z.unknown()).optional()
886
+ },
887
+ (args) => {
888
+ const method = String(args.method ?? "GET").toUpperCase();
889
+ const path = normalizePath(requireString(args.path, "path"));
890
+ if (!RAW_PATH_ALLOWLIST.test(path)) {
891
+ return makeResult({ ok: false, tool: "moltbook_raw_request", error: { code: "path_not_allowed", message: "Path is outside raw request allowlist" } }, true);
892
+ }
893
+ return runApiTool("moltbook_raw_request", method, path, { query: args.query ?? null, body: args.body ?? null, isWrite: method !== "GET" });
894
+ }
895
+ );
896
+ }
897
+ function registerTools(server2) {
898
+ registerHealth(server2);
899
+ registerWriteGuardStatus(server2);
900
+ registerChallengeStatus(server2);
901
+ registerVerify(server2);
902
+ registerAccount(server2);
903
+ registerPosts(server2);
904
+ registerComments(server2);
905
+ registerVotes(server2);
906
+ registerSearch(server2);
907
+ registerSubmolts(server2);
908
+ registerSocial(server2);
909
+ registerRawRequest(server2);
910
+ }
911
+
912
+ // src/server.ts
913
+ function createServer() {
914
+ const server2 = new McpServer({ name: "moltbook", version: "0.1.0" });
915
+ registerTools(server2);
916
+ return server2;
917
+ }
918
+
919
+ // src/index.ts
920
+ var server = createServer();
921
+ var transport = new StdioServerTransport();
922
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "moltbook-mcp",
3
+ "version": "0.1.0",
4
+ "description": "Challenge-aware MCP server for Moltbook with write safety guards",
5
+ "type": "module",
6
+ "bin": {
7
+ "moltbook-mcp": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "engines": {
13
+ "node": ">=22"
14
+ },
15
+ "scripts": {
16
+ "build": "tsup",
17
+ "typecheck": "tsc --noEmit",
18
+ "prepublishOnly": "npm run typecheck && npm run build",
19
+ "start": "node dist/index.js",
20
+ "test": "vitest run",
21
+ "test:watch": "vitest",
22
+ "test:coverage": "vitest run --coverage"
23
+ },
24
+ "dependencies": {
25
+ "@modelcontextprotocol/sdk": "^1.26.0",
26
+ "zod": "^3.25.0"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^22.0.0",
30
+ "@vitest/coverage-v8": "^4.0.18",
31
+ "tsup": "^8.0.0",
32
+ "typescript": "^5.7.0",
33
+ "vitest": "^4.0.18"
34
+ },
35
+ "license": "MIT"
36
+ }