@prmichaelsen/reddit-mcp 0.1.0 → 0.1.1

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 (58) hide show
  1. package/.claude/settings.local.json +4 -1
  2. package/README.md +253 -27
  3. package/agent/progress.yaml +29 -30
  4. package/dist/factory.js +1206 -0
  5. package/dist/factory.js.map +4 -4
  6. package/dist/index.js +1206 -0
  7. package/dist/index.js.map +4 -4
  8. package/dist/server.d.ts.map +1 -1
  9. package/dist/server.js +1206 -0
  10. package/dist/server.js.map +4 -4
  11. package/dist/tools/account.d.ts +4 -0
  12. package/dist/tools/account.d.ts.map +1 -0
  13. package/dist/tools/comments.d.ts +4 -0
  14. package/dist/tools/comments.d.ts.map +1 -0
  15. package/dist/tools/flair.d.ts +4 -0
  16. package/dist/tools/flair.d.ts.map +1 -0
  17. package/dist/tools/messages.d.ts +4 -0
  18. package/dist/tools/messages.d.ts.map +1 -0
  19. package/dist/tools/moderation.d.ts +4 -0
  20. package/dist/tools/moderation.d.ts.map +1 -0
  21. package/dist/tools/multireddits.d.ts +4 -0
  22. package/dist/tools/multireddits.d.ts.map +1 -0
  23. package/dist/tools/posts.d.ts +4 -0
  24. package/dist/tools/posts.d.ts.map +1 -0
  25. package/dist/tools/subreddits.d.ts +4 -0
  26. package/dist/tools/subreddits.d.ts.map +1 -0
  27. package/dist/tools/users.d.ts +4 -0
  28. package/dist/tools/users.d.ts.map +1 -0
  29. package/dist/tools/voting.d.ts +4 -0
  30. package/dist/tools/voting.d.ts.map +1 -0
  31. package/dist/tools/wiki.d.ts +4 -0
  32. package/dist/tools/wiki.d.ts.map +1 -0
  33. package/package.json +1 -1
  34. package/src/server.ts +22 -0
  35. package/src/tools/account.ts +84 -0
  36. package/src/tools/comments.ts +73 -0
  37. package/src/tools/flair.ts +79 -0
  38. package/src/tools/messages.ts +126 -0
  39. package/src/tools/moderation.ts +292 -0
  40. package/src/tools/multireddits.ts +152 -0
  41. package/src/tools/posts.ts +177 -0
  42. package/src/tools/subreddits.ts +137 -0
  43. package/src/tools/users.ts +181 -0
  44. package/src/tools/voting.ts +90 -0
  45. package/src/tools/wiki.ts +118 -0
  46. package/tests/fixtures/reddit-responses.ts +159 -0
  47. package/tests/unit/account.test.ts +95 -0
  48. package/tests/unit/comments.test.ts +92 -0
  49. package/tests/unit/flair.test.ts +101 -0
  50. package/tests/unit/messages.test.ts +106 -0
  51. package/tests/unit/moderation.test.ts +243 -0
  52. package/tests/unit/multireddits.test.ts +136 -0
  53. package/tests/unit/posts.test.ts +155 -0
  54. package/tests/unit/subreddits.test.ts +125 -0
  55. package/tests/unit/transport.test.ts +13 -0
  56. package/tests/unit/users.test.ts +124 -0
  57. package/tests/unit/voting.test.ts +110 -0
  58. package/tests/unit/wiki.test.ts +116 -0
@@ -0,0 +1,292 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import type { RedditClient } from "../client/reddit.js";
4
+
5
+ const paginationParams = {
6
+ limit: z
7
+ .number()
8
+ .min(1)
9
+ .max(100)
10
+ .optional()
11
+ .describe("Number of items to return (1-100, default 25)"),
12
+ after: z.string().optional().describe("Fullname to paginate after"),
13
+ before: z.string().optional().describe("Fullname to paginate before"),
14
+ };
15
+
16
+ function buildParams(
17
+ input: Record<string, unknown>,
18
+ ): Record<string, string | undefined> {
19
+ const params: Record<string, string | undefined> = {};
20
+ for (const [key, value] of Object.entries(input)) {
21
+ if (value !== undefined && value !== null) {
22
+ params[key] = String(value);
23
+ }
24
+ }
25
+ return params;
26
+ }
27
+
28
+ export function registerModerationTools(
29
+ server: McpServer,
30
+ client: RedditClient,
31
+ ): void {
32
+ // === Mod Action Tools (Task 16) ===
33
+
34
+ server.tool(
35
+ "reddit_approve",
36
+ "Approve a post or comment in the mod queue. Requires 'modposts' scope.",
37
+ {
38
+ id: z.string().describe("Fullname of the post (t3_) or comment (t1_) to approve"),
39
+ },
40
+ async (input) => {
41
+ const data = await client.post("/api/approve", { id: input.id });
42
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
43
+ },
44
+ );
45
+
46
+ server.tool(
47
+ "reddit_remove",
48
+ "Remove a post or comment as a moderator. Requires 'modposts' scope.",
49
+ {
50
+ id: z.string().describe("Fullname of the post (t3_) or comment (t1_) to remove"),
51
+ spam: z
52
+ .boolean()
53
+ .optional()
54
+ .describe("If true, mark as spam (default false)"),
55
+ },
56
+ async (input) => {
57
+ const data = await client.post("/api/remove", {
58
+ id: input.id,
59
+ spam: input.spam !== undefined ? String(input.spam) : undefined,
60
+ });
61
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
62
+ },
63
+ );
64
+
65
+ server.tool(
66
+ "reddit_distinguish",
67
+ "Distinguish a post or comment as a moderator. Requires 'modposts' scope.",
68
+ {
69
+ id: z.string().describe("Fullname of the post (t3_) or comment (t1_)"),
70
+ how: z
71
+ .enum(["yes", "no", "admin", "special"])
72
+ .describe("'yes' = mod distinguish, 'no' = remove, 'admin' = admin, 'special' = special"),
73
+ },
74
+ async (input) => {
75
+ const data = await client.post("/api/distinguish", {
76
+ id: input.id,
77
+ how: input.how,
78
+ });
79
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
80
+ },
81
+ );
82
+
83
+ server.tool(
84
+ "reddit_ignore_reports",
85
+ "Ignore future reports on a post or comment. Requires 'modposts' scope.",
86
+ {
87
+ id: z.string().describe("Fullname of the post (t3_) or comment (t1_)"),
88
+ },
89
+ async (input) => {
90
+ const data = await client.post("/api/ignore_reports", { id: input.id });
91
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
92
+ },
93
+ );
94
+
95
+ server.tool(
96
+ "reddit_unignore_reports",
97
+ "Stop ignoring reports on a post or comment. Requires 'modposts' scope.",
98
+ {
99
+ id: z.string().describe("Fullname of the post (t3_) or comment (t1_)"),
100
+ },
101
+ async (input) => {
102
+ const data = await client.post("/api/unignore_reports", { id: input.id });
103
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
104
+ },
105
+ );
106
+
107
+ server.tool(
108
+ "reddit_lock",
109
+ "Lock a post or comment thread (prevent new comments). Requires 'modposts' scope.",
110
+ {
111
+ id: z.string().describe("Fullname of the post (t3_) or comment (t1_) to lock"),
112
+ },
113
+ async (input) => {
114
+ const data = await client.post("/api/lock", { id: input.id });
115
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
116
+ },
117
+ );
118
+
119
+ server.tool(
120
+ "reddit_unlock",
121
+ "Unlock a previously locked post or comment thread. Requires 'modposts' scope.",
122
+ {
123
+ id: z.string().describe("Fullname of the post (t3_) or comment (t1_) to unlock"),
124
+ },
125
+ async (input) => {
126
+ const data = await client.post("/api/unlock", { id: input.id });
127
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
128
+ },
129
+ );
130
+
131
+ // === Mod Listing Tools (Task 17) ===
132
+
133
+ server.tool(
134
+ "reddit_modqueue",
135
+ "Get items in the mod queue awaiting review. Requires 'modposts' scope.",
136
+ {
137
+ subreddit: z.string().describe("Subreddit name (without r/ prefix)"),
138
+ ...paginationParams,
139
+ },
140
+ async (input) => {
141
+ const { subreddit, ...rest } = input;
142
+ const data = await client.get(
143
+ `/r/${subreddit}/about/modqueue`,
144
+ buildParams(rest),
145
+ );
146
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
147
+ },
148
+ );
149
+
150
+ server.tool(
151
+ "reddit_reports",
152
+ "Get reported posts and comments. Requires 'modposts' scope.",
153
+ {
154
+ subreddit: z.string().describe("Subreddit name (without r/ prefix)"),
155
+ ...paginationParams,
156
+ },
157
+ async (input) => {
158
+ const { subreddit, ...rest } = input;
159
+ const data = await client.get(
160
+ `/r/${subreddit}/about/reports`,
161
+ buildParams(rest),
162
+ );
163
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
164
+ },
165
+ );
166
+
167
+ server.tool(
168
+ "reddit_spam",
169
+ "Get items marked as spam. Requires 'modposts' scope.",
170
+ {
171
+ subreddit: z.string().describe("Subreddit name (without r/ prefix)"),
172
+ ...paginationParams,
173
+ },
174
+ async (input) => {
175
+ const { subreddit, ...rest } = input;
176
+ const data = await client.get(
177
+ `/r/${subreddit}/about/spam`,
178
+ buildParams(rest),
179
+ );
180
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
181
+ },
182
+ );
183
+
184
+ server.tool(
185
+ "reddit_edited",
186
+ "Get recently edited posts and comments. Requires 'modposts' scope.",
187
+ {
188
+ subreddit: z.string().describe("Subreddit name (without r/ prefix)"),
189
+ ...paginationParams,
190
+ },
191
+ async (input) => {
192
+ const { subreddit, ...rest } = input;
193
+ const data = await client.get(
194
+ `/r/${subreddit}/about/edited`,
195
+ buildParams(rest),
196
+ );
197
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
198
+ },
199
+ );
200
+
201
+ server.tool(
202
+ "reddit_modlog",
203
+ "Get the moderation log. Requires 'modlog' scope.",
204
+ {
205
+ subreddit: z.string().describe("Subreddit name (without r/ prefix)"),
206
+ type: z
207
+ .string()
208
+ .optional()
209
+ .describe("Filter by action type (e.g. banuser, removelink, approvecomment)"),
210
+ mod: z
211
+ .string()
212
+ .optional()
213
+ .describe("Filter by moderator username"),
214
+ ...paginationParams,
215
+ },
216
+ async (input) => {
217
+ const { subreddit, ...rest } = input;
218
+ const data = await client.get(
219
+ `/r/${subreddit}/about/log`,
220
+ buildParams(rest),
221
+ );
222
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
223
+ },
224
+ );
225
+
226
+ // === Mod Management Tools (Task 18) ===
227
+
228
+ server.tool(
229
+ "reddit_moderators",
230
+ "List moderators of a subreddit. Requires 'read' scope.",
231
+ {
232
+ subreddit: z.string().describe("Subreddit name (without r/ prefix)"),
233
+ },
234
+ async (input) => {
235
+ const data = await client.get(
236
+ `/r/${input.subreddit}/about/moderators`,
237
+ );
238
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
239
+ },
240
+ );
241
+
242
+ server.tool(
243
+ "reddit_contributors",
244
+ "List approved contributors of a subreddit. Requires 'modcontributors' scope.",
245
+ {
246
+ subreddit: z.string().describe("Subreddit name (without r/ prefix)"),
247
+ ...paginationParams,
248
+ },
249
+ async (input) => {
250
+ const { subreddit, ...rest } = input;
251
+ const data = await client.get(
252
+ `/r/${subreddit}/about/contributors`,
253
+ buildParams(rest),
254
+ );
255
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
256
+ },
257
+ );
258
+
259
+ server.tool(
260
+ "reddit_banned",
261
+ "List banned users of a subreddit. Requires 'modcontributors' scope.",
262
+ {
263
+ subreddit: z.string().describe("Subreddit name (without r/ prefix)"),
264
+ ...paginationParams,
265
+ },
266
+ async (input) => {
267
+ const { subreddit, ...rest } = input;
268
+ const data = await client.get(
269
+ `/r/${subreddit}/about/banned`,
270
+ buildParams(rest),
271
+ );
272
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
273
+ },
274
+ );
275
+
276
+ server.tool(
277
+ "reddit_muted",
278
+ "List muted users of a subreddit. Requires 'modcontributors' scope.",
279
+ {
280
+ subreddit: z.string().describe("Subreddit name (without r/ prefix)"),
281
+ ...paginationParams,
282
+ },
283
+ async (input) => {
284
+ const { subreddit, ...rest } = input;
285
+ const data = await client.get(
286
+ `/r/${subreddit}/about/muted`,
287
+ buildParams(rest),
288
+ );
289
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
290
+ },
291
+ );
292
+ }
@@ -0,0 +1,152 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import type { RedditClient } from "../client/reddit.js";
4
+
5
+ export function registerMultiredditTools(
6
+ server: McpServer,
7
+ client: RedditClient,
8
+ ): void {
9
+ server.tool(
10
+ "reddit_multi_mine",
11
+ "List the authenticated user's multireddits (custom feeds). Requires 'read' scope.",
12
+ {},
13
+ async () => {
14
+ const data = await client.get("/api/multi/mine");
15
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
16
+ },
17
+ );
18
+
19
+ server.tool(
20
+ "reddit_multi_get",
21
+ "Get a specific multireddit by path. Requires 'read' scope.",
22
+ {
23
+ multipath: z
24
+ .string()
25
+ .describe("Multireddit path (e.g. /user/username/m/multiname)"),
26
+ },
27
+ async (input) => {
28
+ const data = await client.get(`/api/multi${input.multipath}`);
29
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
30
+ },
31
+ );
32
+
33
+ server.tool(
34
+ "reddit_multi_create",
35
+ "Create a new multireddit. Requires 'subscribe' scope.",
36
+ {
37
+ multipath: z
38
+ .string()
39
+ .describe("Multireddit path (e.g. /user/username/m/multiname)"),
40
+ display_name: z.string().describe("Display name for the multireddit"),
41
+ subreddits: z
42
+ .array(z.string())
43
+ .optional()
44
+ .describe("Array of subreddit names to include"),
45
+ visibility: z
46
+ .enum(["private", "public", "hidden"])
47
+ .optional()
48
+ .describe("Visibility (default private)"),
49
+ description_md: z
50
+ .string()
51
+ .optional()
52
+ .describe("Description in markdown"),
53
+ },
54
+ async (input) => {
55
+ const { multipath, display_name, subreddits, visibility, description_md } = input;
56
+ const model = {
57
+ display_name,
58
+ subreddits: subreddits?.map((name) => ({ name })) ?? [],
59
+ visibility: visibility ?? "private",
60
+ description_md: description_md ?? "",
61
+ };
62
+ const data = await client.post(`/api/multi${multipath}`, {
63
+ model: JSON.stringify(model),
64
+ });
65
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
66
+ },
67
+ );
68
+
69
+ server.tool(
70
+ "reddit_multi_update",
71
+ "Update an existing multireddit. Requires 'subscribe' scope.",
72
+ {
73
+ multipath: z
74
+ .string()
75
+ .describe("Multireddit path (e.g. /user/username/m/multiname)"),
76
+ display_name: z.string().optional().describe("New display name"),
77
+ subreddits: z
78
+ .array(z.string())
79
+ .optional()
80
+ .describe("Updated array of subreddit names"),
81
+ visibility: z
82
+ .enum(["private", "public", "hidden"])
83
+ .optional()
84
+ .describe("Updated visibility"),
85
+ description_md: z
86
+ .string()
87
+ .optional()
88
+ .describe("Updated description in markdown"),
89
+ },
90
+ async (input) => {
91
+ const { multipath, display_name, subreddits, visibility, description_md } = input;
92
+ const model: Record<string, unknown> = {};
93
+ if (display_name !== undefined) model.display_name = display_name;
94
+ if (subreddits !== undefined) model.subreddits = subreddits.map((name) => ({ name }));
95
+ if (visibility !== undefined) model.visibility = visibility;
96
+ if (description_md !== undefined) model.description_md = description_md;
97
+ const data = await client.put(`/api/multi${multipath}`, { model });
98
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
99
+ },
100
+ );
101
+
102
+ server.tool(
103
+ "reddit_multi_delete",
104
+ "Delete a multireddit. Requires 'subscribe' scope.",
105
+ {
106
+ multipath: z
107
+ .string()
108
+ .describe("Multireddit path (e.g. /user/username/m/multiname)"),
109
+ },
110
+ async (input) => {
111
+ const data = await client.delete(`/api/multi${input.multipath}`);
112
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
113
+ },
114
+ );
115
+
116
+ server.tool(
117
+ "reddit_multi_add_sub",
118
+ "Add a subreddit to a multireddit. Requires 'subscribe' scope.",
119
+ {
120
+ multipath: z
121
+ .string()
122
+ .describe("Multireddit path (e.g. /user/username/m/multiname)"),
123
+ srname: z.string().describe("Subreddit name to add (without r/ prefix)"),
124
+ },
125
+ async (input) => {
126
+ const data = await client.put(
127
+ `/api/multi${input.multipath}/r/${input.srname}`,
128
+ { model: { name: input.srname } },
129
+ );
130
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
131
+ },
132
+ );
133
+
134
+ server.tool(
135
+ "reddit_multi_remove_sub",
136
+ "Remove a subreddit from a multireddit. Requires 'subscribe' scope.",
137
+ {
138
+ multipath: z
139
+ .string()
140
+ .describe("Multireddit path (e.g. /user/username/m/multiname)"),
141
+ srname: z
142
+ .string()
143
+ .describe("Subreddit name to remove (without r/ prefix)"),
144
+ },
145
+ async (input) => {
146
+ const data = await client.delete(
147
+ `/api/multi${input.multipath}/r/${input.srname}`,
148
+ );
149
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
150
+ },
151
+ );
152
+ }
@@ -0,0 +1,177 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import type { RedditClient } from "../client/reddit.js";
4
+
5
+ function buildParams(
6
+ input: Record<string, unknown>,
7
+ ): Record<string, string | undefined> {
8
+ const params: Record<string, string | undefined> = {};
9
+ for (const [key, value] of Object.entries(input)) {
10
+ if (value !== undefined && value !== null) {
11
+ params[key] = String(value);
12
+ }
13
+ }
14
+ return params;
15
+ }
16
+
17
+ export function registerPostTools(
18
+ server: McpServer,
19
+ client: RedditClient,
20
+ ): void {
21
+ server.tool(
22
+ "reddit_submit",
23
+ "Create a new post (self-post or link). Requires 'submit' scope.",
24
+ {
25
+ sr: z.string().describe("Subreddit name to post in (without r/ prefix)"),
26
+ title: z.string().describe("Post title"),
27
+ kind: z
28
+ .enum(["self", "link"])
29
+ .describe("Post type: 'self' for text post, 'link' for URL post"),
30
+ text: z
31
+ .string()
32
+ .optional()
33
+ .describe("Post body text (markdown). Required for self posts."),
34
+ url: z
35
+ .string()
36
+ .optional()
37
+ .describe("URL to submit. Required for link posts."),
38
+ nsfw: z
39
+ .boolean()
40
+ .optional()
41
+ .describe("Mark as NSFW"),
42
+ spoiler: z
43
+ .boolean()
44
+ .optional()
45
+ .describe("Mark as spoiler"),
46
+ flair_id: z
47
+ .string()
48
+ .optional()
49
+ .describe("Flair template ID"),
50
+ flair_text: z
51
+ .string()
52
+ .optional()
53
+ .describe("Flair text (if flair allows custom text)"),
54
+ resubmit: z
55
+ .boolean()
56
+ .optional()
57
+ .describe("Allow resubmitting a previously submitted link"),
58
+ sendreplies: z
59
+ .boolean()
60
+ .optional()
61
+ .describe("Send replies to inbox (default true)"),
62
+ },
63
+ async (input) => {
64
+ const data = await client.post("/api/submit", {
65
+ ...buildParams(input),
66
+ api_type: "json",
67
+ });
68
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
69
+ },
70
+ );
71
+
72
+ server.tool(
73
+ "reddit_edit",
74
+ "Edit the body text of a self-post or comment. Requires 'edit' scope.",
75
+ {
76
+ thing_id: z
77
+ .string()
78
+ .describe("Fullname of the post (t3_) or comment (t1_) to edit"),
79
+ text: z.string().describe("New body text (markdown)"),
80
+ },
81
+ async (input) => {
82
+ const data = await client.post("/api/editusertext", {
83
+ ...buildParams(input),
84
+ api_type: "json",
85
+ });
86
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
87
+ },
88
+ );
89
+
90
+ server.tool(
91
+ "reddit_delete",
92
+ "Delete a post or comment. Requires 'edit' scope.",
93
+ {
94
+ id: z
95
+ .string()
96
+ .describe("Fullname of the post (t3_) or comment (t1_) to delete"),
97
+ },
98
+ async (input) => {
99
+ const data = await client.post("/api/del", buildParams(input));
100
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
101
+ },
102
+ );
103
+
104
+ server.tool(
105
+ "reddit_hide",
106
+ "Hide a post from your listings. Requires 'report' scope.",
107
+ {
108
+ id: z.string().describe("Fullname of the post to hide (t3_)"),
109
+ },
110
+ async (input) => {
111
+ const data = await client.post("/api/hide", buildParams(input));
112
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
113
+ },
114
+ );
115
+
116
+ server.tool(
117
+ "reddit_unhide",
118
+ "Unhide a previously hidden post. Requires 'report' scope.",
119
+ {
120
+ id: z.string().describe("Fullname of the post to unhide (t3_)"),
121
+ },
122
+ async (input) => {
123
+ const data = await client.post("/api/unhide", buildParams(input));
124
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
125
+ },
126
+ );
127
+
128
+ server.tool(
129
+ "reddit_mark_nsfw",
130
+ "Mark a post as NSFW. Requires 'modposts' scope.",
131
+ {
132
+ id: z.string().describe("Fullname of the post to mark NSFW (t3_)"),
133
+ },
134
+ async (input) => {
135
+ const data = await client.post("/api/marknsfw", buildParams(input));
136
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
137
+ },
138
+ );
139
+
140
+ server.tool(
141
+ "reddit_unmark_nsfw",
142
+ "Remove NSFW mark from a post. Requires 'modposts' scope.",
143
+ {
144
+ id: z.string().describe("Fullname of the post to unmark NSFW (t3_)"),
145
+ },
146
+ async (input) => {
147
+ const data = await client.post("/api/unmarknsfw", buildParams(input));
148
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
149
+ },
150
+ );
151
+
152
+ server.tool(
153
+ "reddit_spoiler",
154
+ "Mark a post as a spoiler. Requires 'modposts' scope.",
155
+ {
156
+ id: z.string().describe("Fullname of the post to mark as spoiler (t3_)"),
157
+ },
158
+ async (input) => {
159
+ const data = await client.post("/api/spoiler", buildParams(input));
160
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
161
+ },
162
+ );
163
+
164
+ server.tool(
165
+ "reddit_unspoiler",
166
+ "Remove spoiler mark from a post. Requires 'modposts' scope.",
167
+ {
168
+ id: z
169
+ .string()
170
+ .describe("Fullname of the post to remove spoiler mark (t3_)"),
171
+ },
172
+ async (input) => {
173
+ const data = await client.post("/api/unspoiler", buildParams(input));
174
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
175
+ },
176
+ );
177
+ }