feat: add polls, deep-dive, ticket messages, and mod API key tools
CI / Build & Test (push) Successful in 17s
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:
@@ -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);
|
||||
}),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user