bc42508849
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>
204 lines
8.3 KiB
TypeScript
204 lines
8.3 KiB
TypeScript
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);
|
|
}),
|
|
);
|
|
}
|