fix: apply all A-J improvements across MCP server

- A: fix get_user_profile URL (/api/user/profile → /api/users/:username)
- B: fix unban_user to use DELETE /api/mod/users/:id/ban (was POST .../unban)
- C: add check_duplicates tool (GET /api/news/:id/check-duplicates)
- D: add shadowban_user tool (PATCH /api/mod/users/:id/shadowban)
- E: add create_mod_user tool (POST /api/mod/users)
- F: extract handleTool helper in utils.ts; all 37 tools now use it for
     consistent isError:true error responses visible to the AI model
- G: only send Content-Type header when request has a body
- H: support BETTERNEWS_BASE_URL env var for local dev overrides
- I: fix get_news_item_stats description (accessible by own author, not Mod/Admin only)
- J: add .describe() to page/limit/status params in list_all_tickets and list_my_tickets

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Space-Banane
2026-06-17 22:07:21 +02:00
parent 81ac5973e6
commit a1d47d4819
10 changed files with 254 additions and 185 deletions
+69 -56
View File
@@ -1,16 +1,17 @@
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 registerNewsTools(server: McpServer, client: BetterNewsClient) {
server.tool(
"get_top_news",
"Get the top 5 news items by weighted score (likes + recency). No auth required.",
{},
async () => {
handleTool(async () => {
const data = await client.get<unknown>("/api/news/top");
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
return JSON.stringify(data, null, 2);
}),
);
server.tool(
@@ -28,10 +29,10 @@ export function registerNewsTools(server: McpServer, client: BetterNewsClient) {
.describe("Filter by status (Mod/Admin only for non-published)"),
search: z.string().optional().describe("Text search over title and summary"),
},
async (args) => {
handleTool(async (args) => {
const data = await client.get<unknown>("/api/news", args);
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
return JSON.stringify(data, null, 2);
}),
);
server.tool(
@@ -40,10 +41,10 @@ export function registerNewsTools(server: McpServer, client: BetterNewsClient) {
{
id: z.string().uuid().describe("News item UUID"),
},
async ({ id }) => {
handleTool(async ({ id }) => {
const data = await client.get<unknown>(`/api/news/${id}`);
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
return JSON.stringify(data, null, 2);
}),
);
server.tool(
@@ -53,10 +54,10 @@ export function registerNewsTools(server: McpServer, client: BetterNewsClient) {
q: z.string().describe("Search query"),
limit: z.number().int().min(1).max(20).optional().describe("Max results (default 10)"),
},
async ({ q, limit }) => {
handleTool(async ({ q, limit }) => {
const data = await client.get<unknown>("/api/news/search", { q, limit });
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
return JSON.stringify(data, null, 2);
}),
);
server.tool(
@@ -66,10 +67,22 @@ export function registerNewsTools(server: McpServer, client: BetterNewsClient) {
id: z.string().uuid().describe("News item UUID"),
limit: z.number().int().min(1).max(10).optional().describe("Max results (default 5)"),
},
async ({ id, limit }) => {
handleTool(async ({ id, limit }) => {
const data = await client.get<unknown>(`/api/news/${id}/similar`, { limit });
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
return JSON.stringify(data, null, 2);
}),
);
server.tool(
"check_duplicates",
"Check a draft news item for near-duplicate published or pending articles using vector similarity. Call this before submitting a draft for review. Requires news:moderate scope.",
{
id: z.string().uuid().describe("News item UUID to check"),
},
handleTool(async ({ id }) => {
const data = await client.get<unknown>(`/api/news/${id}/check-duplicates`);
return JSON.stringify(data, null, 2);
}),
);
server.tool(
@@ -91,10 +104,10 @@ export function registerNewsTools(server: McpServer, client: BetterNewsClient) {
imageCredits: z.string().optional().describe("Image attribution"),
datePublished: z.string().datetime().optional().describe("Original publication date (ISO 8601)"),
},
async (args) => {
handleTool(async (args) => {
const data = await client.post<unknown>("/api/news", args);
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
return JSON.stringify(data, null, 2);
}),
);
server.tool(
@@ -103,10 +116,10 @@ export function registerNewsTools(server: McpServer, client: BetterNewsClient) {
{
id: z.string().uuid().describe("News item UUID"),
},
async ({ id }) => {
handleTool(async ({ id }) => {
const data = await client.post<unknown>(`/api/news/${id}/submit`);
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
return JSON.stringify(data, null, 2);
}),
);
server.tool(
@@ -114,25 +127,25 @@ export function registerNewsTools(server: McpServer, client: BetterNewsClient) {
"Edit a news item. Owners can only edit draft/needs_revision items. Mod/Admin can edit any. Requires news:write scope.",
{
id: z.string().uuid().describe("News item UUID"),
title: z.string().min(1).max(200).optional(),
readMoreUrl: z.string().url().optional(),
sourceId: z.string().uuid().optional(),
summary: z.string().max(1000).optional(),
longDescription: z.string().optional(),
authorsThought: z.string().max(500).optional(),
category: z.string().optional(),
tags: z.array(z.string()).optional(),
language: z.string().optional(),
country: z.string().optional(),
imageUrl: z.string().url().nullable().optional(),
imageAltText: z.string().nullable().optional(),
imageCredits: z.string().nullable().optional(),
datePublished: z.string().datetime().nullable().optional(),
title: z.string().min(1).max(200).optional().describe("Article title"),
readMoreUrl: z.string().url().optional().describe("URL to the original article"),
sourceId: z.string().uuid().optional().describe("Source UUID"),
summary: z.string().max(1000).optional().describe("Short summary"),
longDescription: z.string().optional().describe("Detailed description"),
authorsThought: z.string().max(500).optional().describe("Author commentary"),
category: z.string().optional().describe("Category slug"),
tags: z.array(z.string()).optional().describe("Array of tag strings"),
language: z.string().optional().describe("ISO 639-1 language code"),
country: z.string().optional().describe("ISO 3166-1 alpha-2 country code"),
imageUrl: z.string().url().nullable().optional().describe("Hero image URL"),
imageAltText: z.string().nullable().optional().describe("Image alt text"),
imageCredits: z.string().nullable().optional().describe("Image attribution"),
datePublished: z.string().datetime().nullable().optional().describe("Original publication date (ISO 8601)"),
},
async ({ id, ...body }) => {
handleTool(async ({ id, ...body }) => {
const data = await client.patch<unknown>(`/api/news/${id}`, body);
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
return JSON.stringify(data, null, 2);
}),
);
server.tool(
@@ -143,10 +156,10 @@ export function registerNewsTools(server: McpServer, client: BetterNewsClient) {
action: z.enum(["approve", "reject", "needs_revision"]).describe("Review decision"),
comment: z.string().optional().describe("Optional feedback comment"),
},
async ({ id, ...body }) => {
handleTool(async ({ id, ...body }) => {
const data = await client.post<unknown>(`/api/news/${id}/review`, body);
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
return JSON.stringify(data, null, 2);
}),
);
server.tool(
@@ -155,10 +168,10 @@ export function registerNewsTools(server: McpServer, client: BetterNewsClient) {
{
id: z.string().uuid().describe("News item UUID"),
},
async ({ id }) => {
handleTool(async ({ id }) => {
const data = await client.post<unknown>(`/api/news/${id}/like`);
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
return JSON.stringify(data, null, 2);
}),
);
server.tool(
@@ -167,10 +180,10 @@ export function registerNewsTools(server: McpServer, client: BetterNewsClient) {
{
id: z.string().uuid().describe("News item UUID"),
},
async ({ id }) => {
handleTool(async ({ id }) => {
const data = await client.post<unknown>(`/api/news/${id}/dislike`);
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
return JSON.stringify(data, null, 2);
}),
);
server.tool(
@@ -179,10 +192,10 @@ export function registerNewsTools(server: McpServer, client: BetterNewsClient) {
{
id: z.string().uuid().describe("News item UUID"),
},
async ({ id }) => {
handleTool(async ({ id }) => {
const data = await client.post<unknown>(`/api/news/${id}/bookmark`);
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
return JSON.stringify(data, null, 2);
}),
);
server.tool(
@@ -191,21 +204,21 @@ export function registerNewsTools(server: McpServer, client: BetterNewsClient) {
{
id: z.string().uuid().describe("News item UUID"),
},
async ({ id }) => {
handleTool(async ({ id }) => {
const data = await client.delete<unknown>(`/api/news/${id}`);
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
return JSON.stringify(data, null, 2);
}),
);
server.tool(
"get_news_item_stats",
"Get engagement statistics for a news item. Requires Mod/Admin role.",
"Get engagement statistics for a news item. Accessible by the item's own author or Mod/Admin. Requires news:read scope.",
{
id: z.string().uuid().describe("News item UUID"),
},
async ({ id }) => {
handleTool(async ({ id }) => {
const data = await client.get<unknown>(`/api/news/${id}/stats`);
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
return JSON.stringify(data, null, 2);
}),
);
}