feat: add discussion tools for managing discussion channels and messages
CI / Build & Test (push) Successful in 15s
CI / Build & Test (push) Successful in 15s
This commit is contained in:
@@ -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);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user