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:
+57
-27
@@ -1,6 +1,7 @@
|
||||
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 registerModerationTools(server: McpServer, client: BetterNewsClient) {
|
||||
server.tool(
|
||||
@@ -10,20 +11,20 @@ export function registerModerationTools(server: McpServer, client: BetterNewsCli
|
||||
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)"),
|
||||
},
|
||||
async (args) => {
|
||||
handleTool(async (args) => {
|
||||
const data = await client.get<unknown>("/api/mod/queue", args);
|
||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
||||
},
|
||||
return JSON.stringify(data, null, 2);
|
||||
}),
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"get_mod_stats",
|
||||
"Get aggregate moderation statistics. Requires Mod/Admin role and mod:stats scope.",
|
||||
{},
|
||||
async () => {
|
||||
handleTool(async () => {
|
||||
const data = await client.get<unknown>("/api/mod/stats");
|
||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
||||
},
|
||||
return JSON.stringify(data, null, 2);
|
||||
}),
|
||||
);
|
||||
|
||||
server.tool(
|
||||
@@ -36,10 +37,10 @@ export function registerModerationTools(server: McpServer, client: BetterNewsCli
|
||||
role: z.enum(["User", "Moderator", "Admin"]).optional().describe("Filter by role"),
|
||||
banned: z.boolean().optional().describe("Filter by ban status"),
|
||||
},
|
||||
async (args) => {
|
||||
handleTool(async (args) => {
|
||||
const data = await client.get<unknown>("/api/mod/users", args);
|
||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
||||
},
|
||||
return JSON.stringify(data, null, 2);
|
||||
}),
|
||||
);
|
||||
|
||||
server.tool(
|
||||
@@ -48,10 +49,10 @@ export function registerModerationTools(server: McpServer, client: BetterNewsCli
|
||||
{
|
||||
userId: z.string().uuid().describe("User UUID"),
|
||||
},
|
||||
async ({ userId }) => {
|
||||
handleTool(async ({ userId }) => {
|
||||
const data = await client.get<unknown>(`/api/mod/users/${userId}`);
|
||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
||||
},
|
||||
return JSON.stringify(data, null, 2);
|
||||
}),
|
||||
);
|
||||
|
||||
server.tool(
|
||||
@@ -61,10 +62,10 @@ export function registerModerationTools(server: McpServer, client: BetterNewsCli
|
||||
userId: z.string().uuid().describe("User UUID"),
|
||||
role: z.enum(["User", "Moderator", "Admin"]).describe("New role"),
|
||||
},
|
||||
async ({ userId, role }) => {
|
||||
handleTool(async ({ userId, role }) => {
|
||||
const data = await client.patch<unknown>(`/api/mod/users/${userId}/role`, { role });
|
||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
||||
},
|
||||
return JSON.stringify(data, null, 2);
|
||||
}),
|
||||
);
|
||||
|
||||
server.tool(
|
||||
@@ -76,10 +77,10 @@ export function registerModerationTools(server: McpServer, client: BetterNewsCli
|
||||
bannedUntil: z.string().datetime().optional().describe("Temporary ban expiry (ISO 8601). Omit for permanent."),
|
||||
permanent: z.boolean().optional().describe("Whether the ban is permanent"),
|
||||
},
|
||||
async ({ userId, ...body }) => {
|
||||
handleTool(async ({ userId, ...body }) => {
|
||||
const data = await client.post<unknown>(`/api/mod/users/${userId}/ban`, body);
|
||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
||||
},
|
||||
return JSON.stringify(data, null, 2);
|
||||
}),
|
||||
);
|
||||
|
||||
server.tool(
|
||||
@@ -88,23 +89,52 @@ export function registerModerationTools(server: McpServer, client: BetterNewsCli
|
||||
{
|
||||
userId: z.string().uuid().describe("User UUID"),
|
||||
},
|
||||
async ({ userId }) => {
|
||||
const data = await client.post<unknown>(`/api/mod/users/${userId}/unban`);
|
||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
||||
handleTool(async ({ userId }) => {
|
||||
const data = await client.delete<unknown>(`/api/mod/users/${userId}/ban`);
|
||||
return JSON.stringify(data, null, 2);
|
||||
}),
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"shadowban_user",
|
||||
"Toggle shadowban on a user. Shadowbanned users can post but their content is invisible to others. Requires Admin role and mod:users scope.",
|
||||
{
|
||||
userId: z.string().uuid().describe("User UUID"),
|
||||
shadowbanned: z.boolean().describe("True to shadowban, false to remove shadowban"),
|
||||
},
|
||||
handleTool(async ({ userId, shadowbanned }) => {
|
||||
const data = await client.patch<unknown>(`/api/mod/users/${userId}/shadowban`, { shadowbanned });
|
||||
return JSON.stringify(data, null, 2);
|
||||
}),
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"create_mod_user",
|
||||
"Create a new user account directly (bypasses email verification). Requires Admin role and mod:users scope.",
|
||||
{
|
||||
email: z.string().email().describe("User email address"),
|
||||
username: z.string().min(3).max(30).describe("Username"),
|
||||
displayName: z.string().min(1).max(25).describe("Display name"),
|
||||
password: z.string().min(8).describe("Initial password"),
|
||||
role: z.enum(["User", "Moderator", "Admin"]).optional().describe("Role (default: User)"),
|
||||
},
|
||||
handleTool(async (body) => {
|
||||
const data = await client.post<unknown>("/api/mod/users", body);
|
||||
return JSON.stringify(data, null, 2);
|
||||
}),
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"list_all_tickets",
|
||||
"List all support tickets (mod view). Requires Mod/Admin role and mod:queue scope.",
|
||||
{
|
||||
page: z.number().int().positive().optional(),
|
||||
limit: z.number().int().min(1).max(100).optional(),
|
||||
status: z.enum(["open", "in_progress", "resolved", "closed"]).optional(),
|
||||
page: z.number().int().positive().optional().describe("Page number (default 1)"),
|
||||
limit: z.number().int().min(1).max(100).optional().describe("Items per page (default 20)"),
|
||||
status: z.enum(["open", "in_progress", "resolved", "closed"]).optional().describe("Filter by status"),
|
||||
},
|
||||
async (args) => {
|
||||
handleTool(async (args) => {
|
||||
const data = await client.get<unknown>("/api/mod/tickets", args);
|
||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
||||
},
|
||||
return JSON.stringify(data, null, 2);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user