feat: add polls, deep-dive, ticket messages, and mod API key tools
CI / Build & Test (push) Successful in 17s

- New polls.ts: 13 tools covering full poll lifecycle (create/list/get/
  update/delete/vote/like/dislike/review) and poll comments
- news.ts: get_deep_dive and regenerate_deep_dive for AI analysis
- tickets.ts: create_ticket_message with internal-note and attachment support
- moderation.ts: list/update/revoke mod-managed user API keys
- user.ts: list_my_polls and list_my_poll_votes
- Bump server version to 1.2.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Space-Banane
2026-07-01 22:31:48 +02:00
parent 8083b0b8a8
commit bc42508849
7 changed files with 348 additions and 5 deletions
+203
View File
@@ -0,0 +1,203 @@
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<unknown>("/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<unknown>("/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<unknown>(`/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<unknown>(`/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<unknown>(`/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<unknown>(`/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<unknown>(`/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<unknown>(`/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<unknown>(`/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<unknown>(`/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<unknown>(`/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<unknown>(`/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<unknown>(`/api/polls/${id}/comments/${commentId}`);
return JSON.stringify(data, null, 2);
}),
);
}