Compare commits

...

5 Commits

Author SHA1 Message Date
Space-Banane c6efac0159 fix: allow optional blog image URLs
CI / Build & Test (push) Successful in 18s
2026-07-02 12:22:55 +02:00
Space-Banane bc42508849 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>
2026-07-01 22:31:48 +02:00
Space-Banane 8083b0b8a8 fix: remove stale legal:write from api-keys scope list
CI / Build & Test (push) Successful in 17s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 23:53:58 +02:00
Space-Banane e6f6e1c19f docs: update CLAUDE.md files for static legal model
Remove DB-backed legal document system references across all repos.
Update root CLAUDE.md: replace "Legal System" section with static
document workflow; add step to remind Paul about material-change emails.
Update api/: remove LegalDocument model, Legal route table, legal:write
from permission matrix, whoami grace-period bullet.
Update frontend/: update /terms /privacy page descriptions, remove
legal-service.ts, remove legal types, replace banner/grace-period
behaviors with static model.
Update queue-worker/: remove legal-update.ts worker row and queue ref.
Update mcp-server/: remove Legal tool category, tool count 89 → 85.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 23:52:28 +02:00
Space-Banane 163e145b9e feat: remove 4 legal document MCP tools
Delete tools/legal.ts (get_terms_of_service, get_privacy_policy,
list_legal_documents, accept_legal_documents) and remove the
registerLegalTools call from index.ts. Tool count: 89 → 85.
Legal documents are now static; no API endpoints back them.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 23:41:59 +02:00
10 changed files with 349 additions and 70 deletions
+31 -14
View File
@@ -12,17 +12,17 @@ Auth: `BETTERNEWS_API_KEY` env var, sent as `Authorization: Bearer` on every req
| `src/index.ts` | Entry point. Imports all register functions and calls them against `server` and `client`. | | `src/index.ts` | Entry point. Imports all register functions and calls them against `server` and `client`. |
| `src/client.ts` | `BetterNewsClient``get/post/patch/delete` wrappers with auth header and error handling. | | `src/client.ts` | `BetterNewsClient``get/post/patch/delete` wrappers with auth header and error handling. |
| `src/utils.ts` | `handleTool(fn)` — wraps a handler, catches errors, returns `{ content: [{ type:"text", text }] }`. | | `src/utils.ts` | `handleTool(fn)` — wraps a handler, catches errors, returns `{ content: [{ type:"text", text }] }`. |
| `src/tools/news.ts` | 22 news tools | | `src/tools/news.ts` | 24 news tools |
| `src/tools/comments.ts` | 6 comment tools | | `src/tools/comments.ts` | 6 comment tools |
| `src/tools/sources.ts` | 7 source tools | | `src/tools/sources.ts` | 7 source tools |
| `src/tools/user.ts` | 7 user/profile tools | | `src/tools/user.ts` | 9 user/profile tools |
| `src/tools/moderation.ts` | 11 moderation tools | | `src/tools/moderation.ts` | 14 moderation tools |
| `src/tools/tickets.ts` | 5 support ticket tools | | `src/tools/tickets.ts` | 6 support ticket tools |
| `src/tools/legal.ts` | 4 legal document tools |
| `src/tools/blog.ts` | 7 blog tools | | `src/tools/blog.ts` | 7 blog tools |
| `src/tools/api-keys.ts` | 4 API key tools | | `src/tools/api-keys.ts` | 4 API key tools |
| `src/tools/sessions.ts` | 3 session tools | | `src/tools/sessions.ts` | 3 session tools |
| `src/tools/discussions.ts` | 13 discussion channel tools | | `src/tools/discussions.ts` | 13 discussion channel tools |
| `src/tools/polls.ts` | 13 poll tools |
--- ---
@@ -56,6 +56,8 @@ Format: `tool_name` → file → `METHOD /api/route` → minimum auth
| `archive_news_item` | `POST /api/news/{id}/archive` | `news:moderate` (owner) | | `archive_news_item` | `POST /api/news/{id}/archive` | `news:moderate` (owner) |
| `direct_publish_news_item` | `POST /api/news/{id}/direct-publish` | `news:moderate` | | `direct_publish_news_item` | `POST /api/news/{id}/direct-publish` | `news:moderate` |
| `direct_create_publish_news_item` | `POST /api/news/direct` | `news:moderate` | | `direct_create_publish_news_item` | `POST /api/news/direct` | `news:moderate` |
| `get_deep_dive` | `GET /api/news/{id}/deep-dive` | optional |
| `regenerate_deep_dive` | `POST /api/news/{id}/deep-dive/regenerate` | `news:moderate` Mod/Admin |
### Comments — `tools/comments.ts` ### Comments — `tools/comments.ts`
@@ -91,6 +93,8 @@ Format: `tool_name` → file → `METHOD /api/route` → minimum auth
| `list_bookmarks` | `GET /api/user/bookmarks` | `bookmarks:write` | | `list_bookmarks` | `GET /api/user/bookmarks` | `bookmarks:write` |
| `update_profile` | `PATCH /api/user/profile` | `user:profile` | | `update_profile` | `PATCH /api/user/profile` | `user:profile` |
| `update_privacy_settings` | `PATCH /api/user/privacy` | `user:profile` | | `update_privacy_settings` | `PATCH /api/user/privacy` | `user:profile` |
| `list_my_polls` | `GET /api/user/polls` | `user:profile` |
| `list_my_poll_votes` | `GET /api/user/poll-votes` | `user:profile` |
### Moderation — `tools/moderation.ts` ### Moderation — `tools/moderation.ts`
@@ -107,6 +111,9 @@ Format: `tool_name` → file → `METHOD /api/route` → minimum auth
| `create_mod_user` | `POST /api/mod/users` | `mod:users` Admin | | `create_mod_user` | `POST /api/mod/users` | `mod:users` Admin |
| `list_all_tickets` | `GET /api/mod/tickets` | `mod:queue` Mod/Admin | | `list_all_tickets` | `GET /api/mod/tickets` | `mod:queue` Mod/Admin |
| `list_mod_comments` | `GET /api/mod/comments` | `mod:queue` Mod/Admin | | `list_mod_comments` | `GET /api/mod/comments` | `mod:queue` Mod/Admin |
| `list_mod_user_api_keys` | `GET /api/mod/users/{userId}/api-keys` | `mod:users` Admin |
| `update_mod_user_api_key` | `PATCH /api/mod/users/{userId}/api-keys/{keyId}` | `mod:users` Admin |
| `revoke_mod_user_api_key` | `DELETE /api/mod/users/{userId}/api-keys/{keyId}` | `mod:users` Admin |
### Tickets — `tools/tickets.ts` ### Tickets — `tools/tickets.ts`
@@ -116,17 +123,9 @@ Format: `tool_name` → file → `METHOD /api/route` → minimum auth
| `get_ticket` | `GET /api/tickets/{id}` | `tickets:read` owner or Mod/Admin | | `get_ticket` | `GET /api/tickets/{id}` | `tickets:read` owner or Mod/Admin |
| `create_ticket` | `POST /api/tickets` | `tickets:write` | | `create_ticket` | `POST /api/tickets` | `tickets:write` |
| `reply_to_ticket` | `POST /api/tickets/{id}/reply` | `tickets:write` | | `reply_to_ticket` | `POST /api/tickets/{id}/reply` | `tickets:write` |
| `create_ticket_message` | `POST /api/tickets/{id}/messages` | `tickets:write` / `tickets:moderate` |
| `update_ticket_status` | `PATCH /api/tickets/{id}/status` | `tickets:moderate` Mod/Admin | | `update_ticket_status` | `PATCH /api/tickets/{id}/status` | `tickets:moderate` Mod/Admin |
### Legal — `tools/legal.ts`
| Tool | Route | Auth |
| ---- | ----- | ---- |
| `get_terms_of_service` | `GET /api/legal/current/terms_of_service` | public |
| `get_privacy_policy` | `GET /api/legal/current/privacy_policy` | public |
| `list_legal_documents` | `GET /api/legal` | optional; Mod/Admin see drafts |
| `accept_legal_documents` | `POST /api/legal/accept` | User+ |
### Blog — `tools/blog.ts` ### Blog — `tools/blog.ts`
| Tool | Route | Auth | | Tool | Route | Auth |
@@ -174,6 +173,24 @@ Format: `tool_name` → file → `METHOD /api/route` → minimum auth
| `list_mod_discussions` | `GET /api/mod/discussions` | Mod/Admin | | `list_mod_discussions` | `GET /api/mod/discussions` | Mod/Admin |
| `review_discussion` | `POST /api/mod/discussions/{id}/review` | Mod/Admin | | `review_discussion` | `POST /api/mod/discussions/{id}/review` | Mod/Admin |
### Polls — `tools/polls.ts`
| Tool | Route | Auth |
| ---- | ----- | ---- |
| `list_polls` | `GET /api/polls` | optional; non-published statuses require Mod/Admin |
| `create_poll` | `POST /api/polls` | `polls:write` + email verified |
| `get_poll` | `GET /api/polls/{id}` | optional; non-published only for author or Mod/Admin |
| `update_poll` | `PATCH /api/polls/{id}` | `polls:write` (owner) or `polls:moderate` (Mod/Admin) |
| `delete_poll` | `DELETE /api/polls/{id}` | `polls:write` (owner pending_review only) or Mod/Admin |
| `vote_poll` | `POST /api/polls/{id}/vote` | `polls:write` |
| `like_poll` | `POST /api/polls/{id}/like` | `polls:write` |
| `dislike_poll` | `POST /api/polls/{id}/dislike` | `polls:write` |
| `review_poll` | `POST /api/polls/{id}/review` | `polls:moderate` Mod/Admin |
| `list_poll_comments` | `GET /api/polls/{id}/comments` | optional |
| `create_poll_comment` | `POST /api/polls/{id}/comments` | `polls:comment` + email verified |
| `hide_poll_comment` | `PATCH /api/polls/{id}/comments/{commentId}` | `polls:moderate` Mod/Admin |
| `delete_poll_comment` | `DELETE /api/polls/{id}/comments/{commentId}` | owner or `polls:comment` Mod/Admin |
--- ---
## Adding a New Tool ## Adding a New Tool
+3 -3
View File
@@ -8,11 +8,11 @@ import { registerSourceTools } from "./tools/sources.js";
import { registerUserTools } from "./tools/user.js"; import { registerUserTools } from "./tools/user.js";
import { registerModerationTools } from "./tools/moderation.js"; import { registerModerationTools } from "./tools/moderation.js";
import { registerTicketTools } from "./tools/tickets.js"; import { registerTicketTools } from "./tools/tickets.js";
import { registerLegalTools } from "./tools/legal.js";
import { registerBlogTools } from "./tools/blog.js"; import { registerBlogTools } from "./tools/blog.js";
import { registerApiKeyTools } from "./tools/api-keys.js"; import { registerApiKeyTools } from "./tools/api-keys.js";
import { registerSessionTools } from "./tools/sessions.js"; import { registerSessionTools } from "./tools/sessions.js";
import { registerDiscussionTools } from "./tools/discussions.js"; import { registerDiscussionTools } from "./tools/discussions.js";
import { registerPollTools } from "./tools/polls.js";
const apiKey = process.env.BETTERNEWS_API_KEY; const apiKey = process.env.BETTERNEWS_API_KEY;
if (!apiKey) { if (!apiKey) {
@@ -28,7 +28,7 @@ const client = new BetterNewsClient(apiKey, baseUrl);
const server = new McpServer({ const server = new McpServer({
name: "betternews", name: "betternews",
version: "1.1.0", version: "1.2.0",
}); });
registerNewsTools(server, client); registerNewsTools(server, client);
@@ -37,11 +37,11 @@ registerSourceTools(server, client);
registerUserTools(server, client); registerUserTools(server, client);
registerModerationTools(server, client); registerModerationTools(server, client);
registerTicketTools(server, client); registerTicketTools(server, client);
registerLegalTools(server, client);
registerBlogTools(server, client); registerBlogTools(server, client);
registerApiKeyTools(server, client); registerApiKeyTools(server, client);
registerSessionTools(server, client); registerSessionTools(server, client);
registerDiscussionTools(server, client); registerDiscussionTools(server, client);
registerPollTools(server, client);
const transport = new StdioServerTransport(); const transport = new StdioServerTransport();
await server.connect(transport); await server.connect(transport);
-1
View File
@@ -18,7 +18,6 @@ const SCOPES = [
"mod:stats", "mod:stats",
"mod:users", "mod:users",
"blog:write", "blog:write",
"legal:write",
"user:profile", "user:profile",
"user:apikeys", "user:apikeys",
"privacy:read", "privacy:read",
+1 -1
View File
@@ -37,7 +37,7 @@ export function registerBlogTools(server: McpServer, client: BetterNewsClient) {
title: z.string().min(1).max(150).describe("Post title"), title: z.string().min(1).max(150).describe("Post title"),
content: z.string().min(1).describe("Post body (markdown supported)"), content: z.string().min(1).describe("Post body (markdown supported)"),
excerpt: z.string().max(300).optional().describe("Short excerpt shown in listings"), excerpt: z.string().max(300).optional().describe("Short excerpt shown in listings"),
imageUrl: z.string().url().optional().describe("Hero image URL"), imageUrl: z.string().url().nullable().optional().describe("Hero image URL"),
}, },
handleTool(async (body) => { handleTool(async (body) => {
const data = await client.post<unknown>("/api/blog", body); const data = await client.post<unknown>("/api/blog", body);
-51
View File
@@ -1,51 +0,0 @@
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 registerLegalTools(server: McpServer, client: BetterNewsClient) {
server.tool(
"get_terms_of_service",
"Get the current published Terms of Service document.",
{},
handleTool(async () => {
const data = await client.get<unknown>("/api/legal/current/terms_of_service");
return JSON.stringify(data, null, 2);
}),
);
server.tool(
"get_privacy_policy",
"Get the current published Privacy Policy document.",
{},
handleTool(async () => {
const data = await client.get<unknown>("/api/legal/current/privacy_policy");
return JSON.stringify(data, null, 2);
}),
);
server.tool(
"list_legal_documents",
"List legal documents. Public sees published only; Mod/Admin see all.",
{
type: z.enum(["terms_of_service", "privacy_policy"]).optional().describe("Filter by document type"),
status: z.enum(["draft", "published", "archived"]).optional().describe("Filter by status (Mod/Admin only)"),
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)"),
},
handleTool(async (args) => {
const data = await client.get<unknown>("/api/legal", args);
return JSON.stringify(data, null, 2);
}),
);
server.tool(
"accept_legal_documents",
"Accept the current legal documents (Terms of Service and Privacy Policy). Clears the legalAcceptanceRequired flag.",
{},
handleTool(async () => {
const data = await client.post<unknown>("/api/legal/accept");
return JSON.stringify(data, null, 2);
}),
);
}
+39
View File
@@ -155,4 +155,43 @@ export function registerModerationTools(server: McpServer, client: BetterNewsCli
return JSON.stringify(data, null, 2); return JSON.stringify(data, null, 2);
}), }),
); );
server.tool(
"list_mod_user_api_keys",
"List all API keys belonging to a specific user. Requires Admin role and mod:users scope.",
{
userId: z.string().uuid().describe("Target user UUID"),
},
handleTool(async ({ userId }) => {
const data = await client.get<unknown>(`/api/mod/users/${userId}/api-keys`);
return JSON.stringify(data, null, 2);
}),
);
server.tool(
"update_mod_user_api_key",
"Toggle the rate-limit exemption on a user's API key. Requires Admin role and mod:users scope.",
{
userId: z.string().uuid().describe("Target user UUID"),
keyId: z.string().uuid().describe("API key UUID"),
rateLimitExempt: z.boolean().describe("True to grant unlimited rate-limit; false to restore limits"),
},
handleTool(async ({ userId, keyId, rateLimitExempt }) => {
const data = await client.patch<unknown>(`/api/mod/users/${userId}/api-keys/${keyId}`, { rateLimitExempt });
return JSON.stringify(data, null, 2);
}),
);
server.tool(
"revoke_mod_user_api_key",
"Revoke a specific API key belonging to a user. Requires Admin role and mod:users scope.",
{
userId: z.string().uuid().describe("Target user UUID"),
keyId: z.string().uuid().describe("API key UUID to revoke"),
},
handleTool(async ({ userId, keyId }) => {
const data = await client.delete<unknown>(`/api/mod/users/${userId}/api-keys/${keyId}`);
return JSON.stringify(data, null, 2);
}),
);
} }
+24
View File
@@ -318,4 +318,28 @@ export function registerNewsTools(server: McpServer, client: BetterNewsClient) {
return JSON.stringify(data, null, 2); return JSON.stringify(data, null, 2);
}), }),
); );
server.tool(
"get_deep_dive",
"Get or generate an AI deep-dive analysis for a published news item. Returns sentiment, trust score, have-to-know score, comment sentiment analysis, post stats, author stats, and related articles. Generated on first request and cached.",
{
id: z.string().uuid().describe("News item UUID"),
},
handleTool(async ({ id }) => {
const data = await client.get<unknown>(`/api/news/${id}/deep-dive`);
return JSON.stringify(data, null, 2);
}),
);
server.tool(
"regenerate_deep_dive",
"Force-regenerate the AI deep-dive for a published news item, discarding the cached version. Requires Mod/Admin role and news:moderate scope.",
{
id: z.string().uuid().describe("News item UUID"),
},
handleTool(async ({ id }) => {
const data = await client.post<unknown>(`/api/news/${id}/deep-dive/regenerate`);
return JSON.stringify(data, null, 2);
}),
);
} }
+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);
}),
);
}
+22
View File
@@ -57,6 +57,28 @@ export function registerTicketTools(server: McpServer, client: BetterNewsClient)
}), }),
); );
server.tool(
"create_ticket_message",
"Post a message to a support ticket. Owners can message their own tickets; Mod/Admin can message any. Mod/Admin can mark messages as internal notes (not visible to the ticket author). Closed/resolved tickets cannot receive new messages. Requires tickets:write (owner) or tickets:moderate (Mod/Admin) scope.",
{
id: z.string().uuid().describe("Ticket UUID"),
content: z.string().min(1).max(5000).describe("Message text"),
isInternal: z
.boolean()
.optional()
.describe("Mark as internal note visible only to Mod/Admin (Mod/Admin only)"),
attachments: z
.array(z.string().url())
.max(5)
.optional()
.describe("Optional file attachment URLs (upload via POST /api/files/upload/attachment first)"),
},
handleTool(async ({ id, ...body }) => {
const data = await client.post<unknown>(`/api/tickets/${id}/messages`, body);
return JSON.stringify(data, null, 2);
}),
);
server.tool( server.tool(
"update_ticket_status", "update_ticket_status",
"Update the status of a support ticket. Requires Mod/Admin role and tickets:moderate scope.", "Update the status of a support ticket. Requires Mod/Admin role and tickets:moderate scope.",
+26
View File
@@ -101,4 +101,30 @@ export function registerUserTools(server: McpServer, client: BetterNewsClient) {
return JSON.stringify(data, null, 2); return JSON.stringify(data, null, 2);
}), }),
); );
server.tool(
"list_my_polls",
"List the authenticated user's own polls (authored or co-authored as a moderator who modified the poll). Requires user:profile 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)"),
},
handleTool(async (args) => {
const data = await client.get<unknown>("/api/user/polls", args);
return JSON.stringify(data, null, 2);
}),
);
server.tool(
"list_my_poll_votes",
"List polls the authenticated user has voted on, most recently voted first. Requires user:profile 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)"),
},
handleTool(async (args) => {
const data = await client.get<unknown>("/api/user/poll-votes", args);
return JSON.stringify(data, null, 2);
}),
);
} }