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 registerPollTools(server: McpServer, client: BetterNewsClient) { server.tool( "list_polls", "List polls. Published polls are visible to all; other statuses require Mod/Admin role. Requires polls:read scope.", { 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)"), status: z .enum(["published", "pending_review", "rejected"]) .optional() .describe("Filter by status. Non-published statuses require Mod/Admin role."), }, handleTool(async (args) => { const data = await client.get("/api/polls", args); return JSON.stringify(data, null, 2); }), ); server.tool( "create_poll", "Create a new poll. The poll starts in pending_review and must be approved before it's visible. Requires polls:write scope and a verified email.", { title: z.string().min(1).max(150).describe("Poll title"), description: z.string().min(1).max(2000).describe("Poll description"), options: z .array(z.string().min(1).max(100)) .min(2) .max(6) .describe("Between 2 and 6 answer options"), expiresAt: z .string() .datetime({ offset: true }) .describe("Expiry timestamp (ISO 8601). Must be between now and 7 days from now."), }, handleTool(async (body) => { const data = await client.post("/api/polls", body); return JSON.stringify(data, null, 2); }), ); server.tool( "get_poll", "Get a single poll by ID. Published polls are publicly visible. Pending/rejected polls are only visible to the author and Mod/Admin. Requires polls:read scope.", { id: z.string().uuid().describe("Poll UUID"), }, handleTool(async ({ id }) => { const data = await client.get(`/api/polls/${id}`); return JSON.stringify(data, null, 2); }), ); server.tool( "update_poll", "Update a poll. Owners can only edit polls in pending_review status. Mod/Admin can edit any. Options cannot be changed after a poll is published. Requires polls:write (owner) or polls:moderate (Mod/Admin) scope.", { id: z.string().uuid().describe("Poll UUID"), title: z.string().min(1).max(150).optional().describe("Poll title"), description: z.string().min(1).max(2000).optional().describe("Poll description"), options: z .array(z.string().min(1).max(100)) .min(2) .max(6) .optional() .describe("Answer options (cannot change after publication)"), expiresAt: z .string() .datetime({ offset: true }) .optional() .describe("New expiry timestamp (ISO 8601, must be within 7 days from now)"), }, handleTool(async ({ id, ...body }) => { const data = await client.patch(`/api/polls/${id}`, body); return JSON.stringify(data, null, 2); }), ); server.tool( "delete_poll", "Delete a poll. Owners can only delete polls in pending_review status. Mod/Admin can delete any. Requires polls:write scope.", { id: z.string().uuid().describe("Poll UUID"), }, handleTool(async ({ id }) => { const data = await client.delete(`/api/polls/${id}`); return JSON.stringify(data, null, 2); }), ); server.tool( "vote_poll", "Cast a vote on a published poll. Votes are final and cannot be changed. Requires polls:write scope.", { id: z.string().uuid().describe("Poll UUID"), optionId: z.string().uuid().describe("The UUID of the option to vote for"), }, handleTool(async ({ id, optionId }) => { const data = await client.post(`/api/polls/${id}/vote`, { optionId }); return JSON.stringify(data, null, 2); }), ); server.tool( "like_poll", "Toggle a like on a published poll. Requires polls:write scope.", { id: z.string().uuid().describe("Poll UUID"), }, handleTool(async ({ id }) => { const data = await client.post(`/api/polls/${id}/like`); return JSON.stringify(data, null, 2); }), ); server.tool( "dislike_poll", "Toggle a dislike on a published poll. Requires polls:write scope.", { id: z.string().uuid().describe("Poll UUID"), }, handleTool(async ({ id }) => { const data = await client.post(`/api/polls/${id}/dislike`); return JSON.stringify(data, null, 2); }), ); server.tool( "review_poll", "Approve or reject a pending poll. Requires Mod/Admin role and polls:moderate scope.", { id: z.string().uuid().describe("Poll UUID"), action: z.enum(["approve", "reject"]).describe("Review decision"), comment: z.string().max(1000).optional().describe("Optional feedback comment sent to the author"), }, handleTool(async ({ id, ...body }) => { const data = await client.post(`/api/polls/${id}/review`, body); return JSON.stringify(data, null, 2); }), ); server.tool( "list_poll_comments", "List comments on a poll. Moderators also see hidden comments. Sort options: top, most_disliked, new, oldest.", { id: z.string().uuid().describe("Poll UUID"), 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)"), sort: z .enum(["top", "most_disliked", "new", "oldest"]) .optional() .describe("Sort order: top = most liked (default), most_disliked, new, oldest"), }, handleTool(async ({ id, ...args }) => { const data = await client.get(`/api/polls/${id}/comments`, args); return JSON.stringify(data, null, 2); }), ); server.tool( "create_poll_comment", "Post a comment on a published poll. Requires polls:comment scope and a verified email.", { id: z.string().uuid().describe("Poll UUID"), content: z.string().min(1).max(2000).describe("Comment text"), }, handleTool(async ({ id, content }) => { const data = await client.post(`/api/polls/${id}/comments`, { content }); return JSON.stringify(data, null, 2); }), ); server.tool( "hide_poll_comment", "Hide or unhide a poll comment. Requires Mod/Admin role and polls:moderate scope.", { id: z.string().uuid().describe("Poll UUID"), commentId: z.string().uuid().describe("Comment UUID"), hidden: z.boolean().describe("True to hide the comment, false to unhide it"), }, handleTool(async ({ id, commentId, hidden }) => { const data = await client.patch(`/api/polls/${id}/comments/${commentId}`, { hidden }); return JSON.stringify(data, null, 2); }), ); server.tool( "delete_poll_comment", "Delete a poll comment. Owners can delete their own; Mod/Admin can delete any. Requires polls:comment scope.", { id: z.string().uuid().describe("Poll UUID"), commentId: z.string().uuid().describe("Comment UUID"), }, handleTool(async ({ id, commentId }) => { const data = await client.delete(`/api/polls/${id}/comments/${commentId}`); return JSON.stringify(data, null, 2); }), ); }