feat: add discussion tools for managing discussion channels and messages
CI / Build & Test (push) Successful in 15s

This commit is contained in:
Space-Banane
2026-06-20 19:12:27 +02:00
parent fa43346e51
commit a5b90a2c05
4 changed files with 404 additions and 3 deletions
+4 -3
View File
@@ -10,10 +10,11 @@ export function registerCommentTools(server: McpServer, client: BetterNewsClient
{
newsItemId: z.string().uuid().describe("News item UUID"),
page: z.number().int().positive().optional().describe("Page number (default 1)"),
limit: z.number().int().min(1).max(100).optional().describe("Items per page (default 20)"),
limit: z.number().int().min(1).max(50).optional().describe("Items per page (default 20, max 50)"),
sort: z.enum(["top", "most_disliked", "new", "oldest"]).optional().describe("Sort order: top = most liked (default), most_disliked, new = newest first, oldest"),
},
handleTool(async ({ newsItemId, page, limit }) => {
const data = await client.get<unknown>(`/api/news/${newsItemId}/comments`, { page, limit });
handleTool(async ({ newsItemId, page, limit, sort }) => {
const data = await client.get<unknown>(`/api/news/${newsItemId}/comments`, { page, limit, sort });
return JSON.stringify(data, null, 2);
}),
);
+184
View File
@@ -0,0 +1,184 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import type { BetterNewsClient } from "../client.js";
import { handleTool } from "../utils.js";
export function registerDiscussionTools(server: McpServer, client: BetterNewsClient) {
server.tool(
"list_discussions",
"List approved discussion channels. Mod/Admin also see pending/rejected/closed topics. No auth required.",
{
page: z.number().int().positive().optional().describe("Page number (default 1)"),
limit: z.number().int().min(1).max(50).optional().describe("Items per page (default 20, max 50)"),
},
handleTool(async (args) => {
const data = await client.get<unknown>("/api/discussions", args);
return JSON.stringify(data, null, 2);
}),
);
server.tool(
"propose_discussion",
"Submit a new discussion topic for AI review and moderator approval. Requires authentication. Rate limited to 3 proposals per hour.",
{
title: z.string().min(5).max(120).describe("Topic title"),
description: z.string().min(10).max(1000).describe("What this discussion channel is about"),
imageUrl: z.string().url().max(2048).nullable().optional().describe("Optional cover image URL"),
},
handleTool(async (body) => {
const data = await client.post<unknown>("/api/discussions", body);
return JSON.stringify(data, null, 2);
}),
);
server.tool(
"get_discussion",
"Get a single discussion topic by UUID. Non-mod users can only see approved/closed topics.",
{
id: z.string().uuid().describe("Discussion topic UUID"),
},
handleTool(async ({ id }) => {
const data = await client.get<unknown>(`/api/discussions/${id}`);
return JSON.stringify(data, null, 2);
}),
);
server.tool(
"update_discussion",
"Update a discussion topic's metadata or status. Requires Mod/Admin role.",
{
id: z.string().uuid().describe("Discussion topic UUID"),
title: z.string().min(5).max(120).optional().describe("New title"),
description: z.string().min(10).max(1000).optional().describe("New description"),
imageUrl: z.string().url().max(2048).nullable().optional().describe("Cover image URL (null to remove)"),
status: z.enum(["approved", "rejected", "closed"]).optional().describe("New status"),
slowdownMs: z.number().int().min(0).max(300000).optional().describe("Slowdown in ms between messages (0 = disabled)"),
aiReviewNote: z.string().max(500).optional().describe("Override the AI review note"),
},
handleTool(async ({ id, ...body }) => {
const data = await client.patch<unknown>(`/api/discussions/${id}`, body);
return JSON.stringify(data, null, 2);
}),
);
server.tool(
"delete_discussion",
"Permanently delete a discussion topic and all its messages. Requires Admin role.",
{
id: z.string().uuid().describe("Discussion topic UUID"),
},
handleTool(async ({ id }) => {
const data = await client.delete<unknown>(`/api/discussions/${id}`);
return JSON.stringify(data, null, 2);
}),
);
server.tool(
"list_discussion_messages",
"Get message history in a discussion channel. Supports cursor-based pagination via 'before'. Mod/Admin also see soft-deleted messages.",
{
id: z.string().uuid().describe("Discussion topic UUID"),
before: z.string().datetime().optional().describe("Cursor: load messages before this ISO timestamp"),
limit: z.number().int().min(1).max(100).optional().describe("Max messages to return (default 50, max 100)"),
},
handleTool(async ({ id, ...params }) => {
const data = await client.get<unknown>(`/api/discussions/${id}/messages`, params);
return JSON.stringify(data, null, 2);
}),
);
server.tool(
"delete_discussion_message",
"Soft-delete a message in a discussion channel. Owners can delete their own; Mod/Admin can delete any.",
{
topicId: z.string().uuid().describe("Discussion topic UUID"),
messageId: z.string().uuid().describe("Message UUID"),
},
handleTool(async ({ topicId, messageId }) => {
const data = await client.delete<unknown>(`/api/discussions/${topicId}/messages/${messageId}`);
return JSON.stringify(data, null, 2);
}),
);
server.tool(
"pin_discussion_message",
"Pin or unpin a message in a discussion channel. Requires Mod/Admin role.",
{
topicId: z.string().uuid().describe("Discussion topic UUID"),
messageId: z.string().uuid().describe("Message UUID"),
pinned: z.boolean().describe("True to pin, false to unpin"),
},
handleTool(async ({ topicId, messageId, pinned }) => {
const data = await client.post<unknown>(`/api/discussions/${topicId}/messages/${messageId}/pin`, { pinned });
return JSON.stringify(data, null, 2);
}),
);
server.tool(
"list_discussion_bans",
"List all user bans in a discussion channel. Requires Mod/Admin role.",
{
id: z.string().uuid().describe("Discussion topic UUID"),
},
handleTool(async ({ id }) => {
const data = await client.get<unknown>(`/api/discussions/${id}/bans`);
return JSON.stringify(data, null, 2);
}),
);
server.tool(
"ban_discussion_user",
"Ban a user from a discussion channel. Requires Mod/Admin role.",
{
topicId: z.string().uuid().describe("Discussion topic UUID"),
userId: z.string().uuid().describe("UUID of the user to ban"),
reason: z.string().min(1).max(500).describe("Reason for the ban"),
expiresAt: z.string().datetime().nullish().describe("Optional ban expiry (ISO 8601). Omit for permanent."),
},
handleTool(async ({ topicId, ...body }) => {
const data = await client.post<unknown>(`/api/discussions/${topicId}/bans`, body);
return JSON.stringify(data, null, 2);
}),
);
server.tool(
"unban_discussion_user",
"Remove a user's ban from a discussion channel. Requires Mod/Admin role.",
{
topicId: z.string().uuid().describe("Discussion topic UUID"),
userId: z.string().uuid().describe("UUID of the user to unban"),
},
handleTool(async ({ topicId, userId }) => {
const data = await client.delete<unknown>(`/api/discussions/${topicId}/bans/${userId}`);
return JSON.stringify(data, null, 2);
}),
);
server.tool(
"list_mod_discussions",
"List discussion topic submissions with moderation detail (AI review result, message/ban counts). Requires Mod/Admin role.",
{
status: z.enum(["pending", "approved", "rejected", "closed"]).optional().describe("Filter by status"),
page: z.number().int().positive().optional().describe("Page number (default 1)"),
limit: z.number().int().min(1).max(50).optional().describe("Items per page (default 20, max 50)"),
},
handleTool(async (args) => {
const data = await client.get<unknown>("/api/mod/discussions", args);
return JSON.stringify(data, null, 2);
}),
);
server.tool(
"review_discussion",
"Approve or reject a pending discussion topic submission. Requires Mod/Admin role.",
{
id: z.string().uuid().describe("Discussion topic UUID"),
action: z.enum(["approve", "reject", "reject_with_ai"]).describe("'approve', 'reject' with an optional note, or 'reject_with_ai' to use the stored AI review note as the rejection reason"),
note: z.string().max(500).optional().describe("Optional moderator note (used as rejection reason when action is 'reject')"),
},
handleTool(async ({ id, ...body }) => {
const data = await client.post<unknown>(`/api/mod/discussions/${id}/review`, body);
return JSON.stringify(data, null, 2);
}),
);
}