@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,136 @@
1
+ import { jest, describe, it, expect, beforeEach } from "@jest/globals";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { registerMultiredditTools } from "../../src/tools/multireddits.js";
4
+ import { createMockClient } from "../helpers/mock-client.js";
5
+ import { mockEmptyResponse } from "../fixtures/reddit-responses.js";
6
+
7
+ const mockMultiList = [
8
+ {
9
+ kind: "LabeledMulti",
10
+ data: {
11
+ name: "frontpage",
12
+ display_name: "Frontpage",
13
+ path: "/user/testuser/m/frontpage",
14
+ subreddits: [{ name: "programming" }, { name: "typescript" }],
15
+ visibility: "private",
16
+ },
17
+ },
18
+ ];
19
+
20
+ const mockMulti = {
21
+ kind: "LabeledMulti",
22
+ data: {
23
+ name: "frontpage",
24
+ display_name: "Frontpage",
25
+ path: "/user/testuser/m/frontpage",
26
+ subreddits: [{ name: "programming" }],
27
+ visibility: "private",
28
+ },
29
+ };
30
+
31
+ describe("Multireddit Tools", () => {
32
+ let server: McpServer;
33
+ let mockClient: ReturnType<typeof createMockClient>;
34
+
35
+ beforeEach(() => {
36
+ server = new McpServer({ name: "test", version: "0.0.1" });
37
+ mockClient = createMockClient(
38
+ new Map([
39
+ ["/api/multi/mine", mockMultiList],
40
+ ["/api/multi/user/testuser/m/frontpage", mockMulti],
41
+ ["/api/multi/user/testuser/m/frontpage/r/typescript", mockEmptyResponse],
42
+ ]),
43
+ );
44
+ registerMultiredditTools(server, mockClient);
45
+ });
46
+
47
+ it("registers multireddit tools", () => {
48
+ expect(server).toBeDefined();
49
+ });
50
+
51
+ describe("reddit_multi_mine", () => {
52
+ it("calls GET /api/multi/mine", async () => {
53
+ const result = await mockClient.get("/api/multi/mine");
54
+ expect(mockClient.mockGet).toHaveBeenCalledWith("/api/multi/mine");
55
+ expect(result).toEqual(mockMultiList);
56
+ });
57
+ });
58
+
59
+ describe("reddit_multi_get", () => {
60
+ it("calls GET /api/multi/{multipath}", async () => {
61
+ const result = await mockClient.get(
62
+ "/api/multi/user/testuser/m/frontpage",
63
+ );
64
+ expect(mockClient.mockGet).toHaveBeenCalledWith(
65
+ "/api/multi/user/testuser/m/frontpage",
66
+ );
67
+ expect(result).toEqual(mockMulti);
68
+ });
69
+ });
70
+
71
+ describe("reddit_multi_create", () => {
72
+ it("calls POST /api/multi/{multipath}", async () => {
73
+ await mockClient.post("/api/multi/user/testuser/m/frontpage", {
74
+ model: JSON.stringify({
75
+ display_name: "Frontpage",
76
+ subreddits: [{ name: "programming" }],
77
+ visibility: "private",
78
+ description_md: "",
79
+ }),
80
+ });
81
+ expect(mockClient.mockPost).toHaveBeenCalledWith(
82
+ "/api/multi/user/testuser/m/frontpage",
83
+ expect.objectContaining({
84
+ model: expect.stringContaining("Frontpage"),
85
+ }),
86
+ );
87
+ });
88
+ });
89
+
90
+ describe("reddit_multi_update", () => {
91
+ it("calls PUT /api/multi/{multipath}", async () => {
92
+ await mockClient.put("/api/multi/user/testuser/m/frontpage", {
93
+ model: { display_name: "Updated Name" },
94
+ });
95
+ expect(mockClient.put).toHaveBeenCalledWith(
96
+ "/api/multi/user/testuser/m/frontpage",
97
+ expect.objectContaining({
98
+ model: { display_name: "Updated Name" },
99
+ }),
100
+ );
101
+ });
102
+ });
103
+
104
+ describe("reddit_multi_delete", () => {
105
+ it("calls DELETE /api/multi/{multipath}", async () => {
106
+ await mockClient.delete("/api/multi/user/testuser/m/frontpage");
107
+ expect(mockClient.delete).toHaveBeenCalledWith(
108
+ "/api/multi/user/testuser/m/frontpage",
109
+ );
110
+ });
111
+ });
112
+
113
+ describe("reddit_multi_add_sub", () => {
114
+ it("calls PUT /api/multi/{multipath}/r/{srname}", async () => {
115
+ await mockClient.put(
116
+ "/api/multi/user/testuser/m/frontpage/r/typescript",
117
+ { model: { name: "typescript" } },
118
+ );
119
+ expect(mockClient.put).toHaveBeenCalledWith(
120
+ "/api/multi/user/testuser/m/frontpage/r/typescript",
121
+ { model: { name: "typescript" } },
122
+ );
123
+ });
124
+ });
125
+
126
+ describe("reddit_multi_remove_sub", () => {
127
+ it("calls DELETE /api/multi/{multipath}/r/{srname}", async () => {
128
+ await mockClient.delete(
129
+ "/api/multi/user/testuser/m/frontpage/r/typescript",
130
+ );
131
+ expect(mockClient.delete).toHaveBeenCalledWith(
132
+ "/api/multi/user/testuser/m/frontpage/r/typescript",
133
+ );
134
+ });
135
+ });
136
+ });
@@ -0,0 +1,155 @@
1
+ import { describe, it, expect, beforeEach } from "@jest/globals";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { registerPostTools } from "../../src/tools/posts.js";
4
+ import { createMockClient } from "../helpers/mock-client.js";
5
+ import {
6
+ mockSubmitResponse,
7
+ mockEditResponse,
8
+ mockEmptyResponse,
9
+ } from "../fixtures/reddit-responses.js";
10
+
11
+ describe("Post Tools", () => {
12
+ let server: McpServer;
13
+ let mockClient: ReturnType<typeof createMockClient>;
14
+
15
+ beforeEach(() => {
16
+ server = new McpServer({ name: "test", version: "0.0.1" });
17
+ mockClient = createMockClient(
18
+ new Map([
19
+ ["/api/submit", mockSubmitResponse],
20
+ ["/api/editusertext", mockEditResponse],
21
+ ["/api/del", mockEmptyResponse],
22
+ ["/api/hide", mockEmptyResponse],
23
+ ["/api/unhide", mockEmptyResponse],
24
+ ["/api/marknsfw", mockEmptyResponse],
25
+ ["/api/unmarknsfw", mockEmptyResponse],
26
+ ["/api/spoiler", mockEmptyResponse],
27
+ ["/api/unspoiler", mockEmptyResponse],
28
+ ]),
29
+ );
30
+ registerPostTools(server, mockClient);
31
+ });
32
+
33
+ it("registers post tools", () => {
34
+ expect(server).toBeDefined();
35
+ });
36
+
37
+ describe("reddit_submit", () => {
38
+ it("calls POST /api/submit for self post", async () => {
39
+ await mockClient.post("/api/submit", {
40
+ sr: "test",
41
+ title: "My Post",
42
+ kind: "self",
43
+ text: "Hello world",
44
+ api_type: "json",
45
+ });
46
+ expect(mockClient.mockPost).toHaveBeenCalledWith(
47
+ "/api/submit",
48
+ expect.objectContaining({
49
+ sr: "test",
50
+ title: "My Post",
51
+ kind: "self",
52
+ text: "Hello world",
53
+ }),
54
+ );
55
+ });
56
+
57
+ it("calls POST /api/submit for link post", async () => {
58
+ await mockClient.post("/api/submit", {
59
+ sr: "test",
60
+ title: "My Link",
61
+ kind: "link",
62
+ url: "https://example.com",
63
+ api_type: "json",
64
+ });
65
+ expect(mockClient.mockPost).toHaveBeenCalledWith(
66
+ "/api/submit",
67
+ expect.objectContaining({
68
+ sr: "test",
69
+ kind: "link",
70
+ url: "https://example.com",
71
+ }),
72
+ );
73
+ });
74
+ });
75
+
76
+ describe("reddit_edit", () => {
77
+ it("calls POST /api/editusertext", async () => {
78
+ await mockClient.post("/api/editusertext", {
79
+ thing_id: "t3_abc123",
80
+ text: "Updated text",
81
+ api_type: "json",
82
+ });
83
+ expect(mockClient.mockPost).toHaveBeenCalledWith(
84
+ "/api/editusertext",
85
+ expect.objectContaining({
86
+ thing_id: "t3_abc123",
87
+ text: "Updated text",
88
+ }),
89
+ );
90
+ });
91
+ });
92
+
93
+ describe("reddit_delete", () => {
94
+ it("calls POST /api/del", async () => {
95
+ await mockClient.post("/api/del", { id: "t3_abc123" });
96
+ expect(mockClient.mockPost).toHaveBeenCalledWith("/api/del", {
97
+ id: "t3_abc123",
98
+ });
99
+ });
100
+ });
101
+
102
+ describe("reddit_hide", () => {
103
+ it("calls POST /api/hide", async () => {
104
+ await mockClient.post("/api/hide", { id: "t3_abc123" });
105
+ expect(mockClient.mockPost).toHaveBeenCalledWith("/api/hide", {
106
+ id: "t3_abc123",
107
+ });
108
+ });
109
+ });
110
+
111
+ describe("reddit_unhide", () => {
112
+ it("calls POST /api/unhide", async () => {
113
+ await mockClient.post("/api/unhide", { id: "t3_abc123" });
114
+ expect(mockClient.mockPost).toHaveBeenCalledWith("/api/unhide", {
115
+ id: "t3_abc123",
116
+ });
117
+ });
118
+ });
119
+
120
+ describe("reddit_mark_nsfw", () => {
121
+ it("calls POST /api/marknsfw", async () => {
122
+ await mockClient.post("/api/marknsfw", { id: "t3_abc123" });
123
+ expect(mockClient.mockPost).toHaveBeenCalledWith("/api/marknsfw", {
124
+ id: "t3_abc123",
125
+ });
126
+ });
127
+ });
128
+
129
+ describe("reddit_unmark_nsfw", () => {
130
+ it("calls POST /api/unmarknsfw", async () => {
131
+ await mockClient.post("/api/unmarknsfw", { id: "t3_abc123" });
132
+ expect(mockClient.mockPost).toHaveBeenCalledWith("/api/unmarknsfw", {
133
+ id: "t3_abc123",
134
+ });
135
+ });
136
+ });
137
+
138
+ describe("reddit_spoiler", () => {
139
+ it("calls POST /api/spoiler", async () => {
140
+ await mockClient.post("/api/spoiler", { id: "t3_abc123" });
141
+ expect(mockClient.mockPost).toHaveBeenCalledWith("/api/spoiler", {
142
+ id: "t3_abc123",
143
+ });
144
+ });
145
+ });
146
+
147
+ describe("reddit_unspoiler", () => {
148
+ it("calls POST /api/unspoiler", async () => {
149
+ await mockClient.post("/api/unspoiler", { id: "t3_abc123" });
150
+ expect(mockClient.mockPost).toHaveBeenCalledWith("/api/unspoiler", {
151
+ id: "t3_abc123",
152
+ });
153
+ });
154
+ });
155
+ });
@@ -0,0 +1,125 @@
1
+ import { describe, it, expect, beforeEach } from "@jest/globals";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { registerSubredditTools } from "../../src/tools/subreddits.js";
4
+ import { createMockClient } from "../helpers/mock-client.js";
5
+ import {
6
+ mockSubredditAbout,
7
+ mockListing,
8
+ mockEmptyResponse,
9
+ } from "../fixtures/reddit-responses.js";
10
+
11
+ const mockRulesResponse = {
12
+ rules: [
13
+ {
14
+ kind: "all",
15
+ description: "No spam allowed",
16
+ short_name: "No Spam",
17
+ violation_reason: "Spam",
18
+ priority: 0,
19
+ },
20
+ ],
21
+ site_rules: ["Content policy"],
22
+ };
23
+
24
+ describe("Subreddit Tools", () => {
25
+ let server: McpServer;
26
+ let mockClient: ReturnType<typeof createMockClient>;
27
+
28
+ beforeEach(() => {
29
+ server = new McpServer({ name: "test", version: "0.0.1" });
30
+ mockClient = createMockClient(
31
+ new Map([
32
+ ["/r/test/about", mockSubredditAbout],
33
+ ["/r/test/about/rules", mockRulesResponse],
34
+ ["/api/subscribe", mockEmptyResponse],
35
+ ["/subreddits/mine/subscriber", mockListing],
36
+ ["/subreddits/popular", mockListing],
37
+ ["/subreddits/new", mockListing],
38
+ ["/subreddits/search", mockListing],
39
+ ]),
40
+ );
41
+ registerSubredditTools(server, mockClient);
42
+ });
43
+
44
+ it("registers subreddit tools", () => {
45
+ expect(server).toBeDefined();
46
+ });
47
+
48
+ describe("reddit_subreddit_about", () => {
49
+ it("calls GET /r/{subreddit}/about", async () => {
50
+ const result = await mockClient.get("/r/test/about");
51
+ expect(mockClient.mockGet).toHaveBeenCalledWith("/r/test/about");
52
+ expect(result).toEqual(mockSubredditAbout);
53
+ });
54
+ });
55
+
56
+ describe("reddit_subreddit_rules", () => {
57
+ it("calls GET /r/{subreddit}/about/rules", async () => {
58
+ const result = await mockClient.get("/r/test/about/rules");
59
+ expect(mockClient.mockGet).toHaveBeenCalledWith("/r/test/about/rules");
60
+ expect(result).toEqual(mockRulesResponse);
61
+ });
62
+ });
63
+
64
+ describe("reddit_subscribe", () => {
65
+ it("calls POST /api/subscribe with action=sub", async () => {
66
+ await mockClient.post("/api/subscribe", {
67
+ sr_name: "test",
68
+ action: "sub",
69
+ });
70
+ expect(mockClient.mockPost).toHaveBeenCalledWith("/api/subscribe", {
71
+ sr_name: "test",
72
+ action: "sub",
73
+ });
74
+ });
75
+ });
76
+
77
+ describe("reddit_unsubscribe", () => {
78
+ it("calls POST /api/subscribe with action=unsub", async () => {
79
+ await mockClient.post("/api/subscribe", {
80
+ sr_name: "test",
81
+ action: "unsub",
82
+ });
83
+ expect(mockClient.mockPost).toHaveBeenCalledWith("/api/subscribe", {
84
+ sr_name: "test",
85
+ action: "unsub",
86
+ });
87
+ });
88
+ });
89
+
90
+ describe("reddit_subreddits_mine", () => {
91
+ it("calls GET /subreddits/mine/subscriber", async () => {
92
+ await mockClient.get("/subreddits/mine/subscriber", { limit: "25" });
93
+ expect(mockClient.mockGet).toHaveBeenCalledWith(
94
+ "/subreddits/mine/subscriber",
95
+ { limit: "25" },
96
+ );
97
+ });
98
+ });
99
+
100
+ describe("reddit_subreddits_popular", () => {
101
+ it("calls GET /subreddits/popular", async () => {
102
+ await mockClient.get("/subreddits/popular", {});
103
+ expect(mockClient.mockGet).toHaveBeenCalledWith(
104
+ "/subreddits/popular",
105
+ {},
106
+ );
107
+ });
108
+ });
109
+
110
+ describe("reddit_subreddits_new", () => {
111
+ it("calls GET /subreddits/new", async () => {
112
+ await mockClient.get("/subreddits/new", {});
113
+ expect(mockClient.mockGet).toHaveBeenCalledWith("/subreddits/new", {});
114
+ });
115
+ });
116
+
117
+ describe("reddit_subreddits_search", () => {
118
+ it("calls GET /subreddits/search with query", async () => {
119
+ await mockClient.get("/subreddits/search", { q: "programming" });
120
+ expect(mockClient.mockGet).toHaveBeenCalledWith("/subreddits/search", {
121
+ q: "programming",
122
+ });
123
+ });
124
+ });
125
+ });
@@ -0,0 +1,13 @@
1
+ import { describe, it, expect } from "@jest/globals";
2
+ import { startHttpTransport } from "../../src/transport/http.js";
3
+
4
+ describe("HTTP Transport", () => {
5
+ it("exports startHttpTransport function", () => {
6
+ expect(typeof startHttpTransport).toBe("function");
7
+ });
8
+
9
+ it("accepts server and options parameters", () => {
10
+ // Verify the function signature matches expectations
11
+ expect(startHttpTransport.length).toBeGreaterThanOrEqual(1);
12
+ });
13
+ });
@@ -0,0 +1,124 @@
1
+ import { describe, it, expect, beforeEach } from "@jest/globals";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { registerUserTools } from "../../src/tools/users.js";
4
+ import { createMockClient } from "../helpers/mock-client.js";
5
+ import {
6
+ mockUserProfile,
7
+ mockListing,
8
+ mockTrophiesResponse,
9
+ mockEmptyResponse,
10
+ } from "../fixtures/reddit-responses.js";
11
+
12
+ describe("User Profile Tools", () => {
13
+ let server: McpServer;
14
+ let mockClient: ReturnType<typeof createMockClient>;
15
+
16
+ beforeEach(() => {
17
+ server = new McpServer({ name: "test", version: "0.0.1" });
18
+ mockClient = createMockClient(
19
+ new Map([
20
+ ["/user/testuser/about", mockUserProfile],
21
+ ["/user/testuser/overview", mockListing],
22
+ ["/user/testuser/submitted", mockListing],
23
+ ["/user/testuser/comments", mockListing],
24
+ ["/user/testuser/saved", mockListing],
25
+ ["/user/testuser/upvoted", mockListing],
26
+ ["/user/testuser/downvoted", mockListing],
27
+ ["/api/v1/user/testuser/trophies", mockTrophiesResponse],
28
+ ["/api/block_user", mockEmptyResponse],
29
+ ]),
30
+ );
31
+ registerUserTools(server, mockClient);
32
+ });
33
+
34
+ it("registers user tools", () => {
35
+ expect(server).toBeDefined();
36
+ });
37
+
38
+ describe("reddit_user_about", () => {
39
+ it("calls GET /user/{username}/about", async () => {
40
+ const result = await mockClient.get("/user/testuser/about");
41
+ expect(mockClient.mockGet).toHaveBeenCalledWith("/user/testuser/about");
42
+ expect(result).toEqual(mockUserProfile);
43
+ });
44
+ });
45
+
46
+ describe("reddit_user_overview", () => {
47
+ it("calls GET /user/{username}/overview", async () => {
48
+ await mockClient.get("/user/testuser/overview", { sort: "new", limit: "10" });
49
+ expect(mockClient.mockGet).toHaveBeenCalledWith(
50
+ "/user/testuser/overview",
51
+ { sort: "new", limit: "10" },
52
+ );
53
+ });
54
+ });
55
+
56
+ describe("reddit_user_submitted", () => {
57
+ it("calls GET /user/{username}/submitted", async () => {
58
+ await mockClient.get("/user/testuser/submitted", { sort: "top", t: "week" });
59
+ expect(mockClient.mockGet).toHaveBeenCalledWith(
60
+ "/user/testuser/submitted",
61
+ { sort: "top", t: "week" },
62
+ );
63
+ });
64
+ });
65
+
66
+ describe("reddit_user_comments", () => {
67
+ it("calls GET /user/{username}/comments", async () => {
68
+ await mockClient.get("/user/testuser/comments", {});
69
+ expect(mockClient.mockGet).toHaveBeenCalledWith(
70
+ "/user/testuser/comments",
71
+ {},
72
+ );
73
+ });
74
+ });
75
+
76
+ describe("reddit_user_saved", () => {
77
+ it("calls GET /user/{username}/saved", async () => {
78
+ await mockClient.get("/user/testuser/saved", {});
79
+ expect(mockClient.mockGet).toHaveBeenCalledWith(
80
+ "/user/testuser/saved",
81
+ {},
82
+ );
83
+ });
84
+ });
85
+
86
+ describe("reddit_user_upvoted", () => {
87
+ it("calls GET /user/{username}/upvoted", async () => {
88
+ await mockClient.get("/user/testuser/upvoted", {});
89
+ expect(mockClient.mockGet).toHaveBeenCalledWith(
90
+ "/user/testuser/upvoted",
91
+ {},
92
+ );
93
+ });
94
+ });
95
+
96
+ describe("reddit_user_downvoted", () => {
97
+ it("calls GET /user/{username}/downvoted", async () => {
98
+ await mockClient.get("/user/testuser/downvoted", {});
99
+ expect(mockClient.mockGet).toHaveBeenCalledWith(
100
+ "/user/testuser/downvoted",
101
+ {},
102
+ );
103
+ });
104
+ });
105
+
106
+ describe("reddit_user_trophies", () => {
107
+ it("calls GET /api/v1/user/{username}/trophies", async () => {
108
+ const result = await mockClient.get("/api/v1/user/testuser/trophies");
109
+ expect(mockClient.mockGet).toHaveBeenCalledWith(
110
+ "/api/v1/user/testuser/trophies",
111
+ );
112
+ expect(result).toEqual(mockTrophiesResponse);
113
+ });
114
+ });
115
+
116
+ describe("reddit_block_user", () => {
117
+ it("calls POST /api/block_user", async () => {
118
+ await mockClient.post("/api/block_user", { name: "spammer" });
119
+ expect(mockClient.mockPost).toHaveBeenCalledWith("/api/block_user", {
120
+ name: "spammer",
121
+ });
122
+ });
123
+ });
124
+ });
@@ -0,0 +1,110 @@
1
+ import { describe, it, expect, beforeEach } from "@jest/globals";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { registerVotingTools } from "../../src/tools/voting.js";
4
+ import { createMockClient } from "../helpers/mock-client.js";
5
+ import { mockEmptyResponse } from "../fixtures/reddit-responses.js";
6
+
7
+ describe("Voting, Save & Report Tools", () => {
8
+ let server: McpServer;
9
+ let mockClient: ReturnType<typeof createMockClient>;
10
+
11
+ beforeEach(() => {
12
+ server = new McpServer({ name: "test", version: "0.0.1" });
13
+ mockClient = createMockClient(
14
+ new Map([
15
+ ["/api/vote", mockEmptyResponse],
16
+ ["/api/save", mockEmptyResponse],
17
+ ["/api/unsave", mockEmptyResponse],
18
+ ["/api/report", mockEmptyResponse],
19
+ ]),
20
+ );
21
+ registerVotingTools(server, mockClient);
22
+ });
23
+
24
+ it("registers voting tools", () => {
25
+ expect(server).toBeDefined();
26
+ });
27
+
28
+ describe("reddit_vote", () => {
29
+ it("calls POST /api/vote for upvote", async () => {
30
+ await mockClient.post("/api/vote", { id: "t3_abc123", dir: "1" });
31
+ expect(mockClient.mockPost).toHaveBeenCalledWith("/api/vote", {
32
+ id: "t3_abc123",
33
+ dir: "1",
34
+ });
35
+ });
36
+
37
+ it("calls POST /api/vote for downvote", async () => {
38
+ await mockClient.post("/api/vote", { id: "t3_abc123", dir: "-1" });
39
+ expect(mockClient.mockPost).toHaveBeenCalledWith("/api/vote", {
40
+ id: "t3_abc123",
41
+ dir: "-1",
42
+ });
43
+ });
44
+
45
+ it("calls POST /api/vote for unvote", async () => {
46
+ await mockClient.post("/api/vote", { id: "t1_xyz789", dir: "0" });
47
+ expect(mockClient.mockPost).toHaveBeenCalledWith("/api/vote", {
48
+ id: "t1_xyz789",
49
+ dir: "0",
50
+ });
51
+ });
52
+ });
53
+
54
+ describe("reddit_save", () => {
55
+ it("calls POST /api/save", async () => {
56
+ await mockClient.post("/api/save", { id: "t3_abc123" });
57
+ expect(mockClient.mockPost).toHaveBeenCalledWith("/api/save", {
58
+ id: "t3_abc123",
59
+ });
60
+ });
61
+
62
+ it("supports category parameter", async () => {
63
+ await mockClient.post("/api/save", {
64
+ id: "t3_abc123",
65
+ category: "favorites",
66
+ });
67
+ expect(mockClient.mockPost).toHaveBeenCalledWith("/api/save", {
68
+ id: "t3_abc123",
69
+ category: "favorites",
70
+ });
71
+ });
72
+ });
73
+
74
+ describe("reddit_unsave", () => {
75
+ it("calls POST /api/unsave", async () => {
76
+ await mockClient.post("/api/unsave", { id: "t3_abc123" });
77
+ expect(mockClient.mockPost).toHaveBeenCalledWith("/api/unsave", {
78
+ id: "t3_abc123",
79
+ });
80
+ });
81
+ });
82
+
83
+ describe("reddit_report", () => {
84
+ it("calls POST /api/report with reason", async () => {
85
+ await mockClient.post("/api/report", {
86
+ thing_id: "t3_abc123",
87
+ reason: "spam",
88
+ });
89
+ expect(mockClient.mockPost).toHaveBeenCalledWith("/api/report", {
90
+ thing_id: "t3_abc123",
91
+ reason: "spam",
92
+ });
93
+ });
94
+
95
+ it("supports other_reason parameter", async () => {
96
+ await mockClient.post("/api/report", {
97
+ thing_id: "t1_xyz789",
98
+ reason: "other",
99
+ other_reason: "Offensive content",
100
+ });
101
+ expect(mockClient.mockPost).toHaveBeenCalledWith(
102
+ "/api/report",
103
+ expect.objectContaining({
104
+ reason: "other",
105
+ other_reason: "Offensive content",
106
+ }),
107
+ );
108
+ });
109
+ });
110
+ });