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.", {}, handleTool(async () => { const data = await client.get("/api/news/top"); return JSON.stringify(data, null, 2); }), ); server.tool( "list_news", "List published news items with optional pagination and filters. Moderators/Admins can also filter by status.", { 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, max 100)"), category: z.string().optional().describe("Filter by category slug"), tag: z.string().optional().describe("Filter by tag"), source: z.string().uuid().optional().describe("Filter by source UUID"), status: z .enum(["published", "draft", "pending_review", "approved", "rejected", "needs_revision", "archived"]) .optional() .describe("Filter by status (Mod/Admin only for non-published)"), search: z.string().optional().describe("Text search over title and summary"), }, handleTool(async (args) => { const data = await client.get("/api/news", args); return JSON.stringify(data, null, 2); }), ); server.tool( "get_news_item", "Get a single news item by ID.", { id: z.string().uuid().describe("News item UUID"), }, handleTool(async ({ id }) => { const data = await client.get(`/api/news/${id}`); return JSON.stringify(data, null, 2); }), ); server.tool( "search_news", "Semantic vector search over published news items.", { q: z.string().describe("Search query"), limit: z.number().int().min(1).max(20).optional().describe("Max results (default 10)"), }, handleTool(async ({ q, limit }) => { const data = await client.get("/api/news/search", { q, limit }); return JSON.stringify(data, null, 2); }), ); server.tool( "get_similar_news", "Get news items similar to a given item using vector similarity.", { id: z.string().uuid().describe("News item UUID"), limit: z.number().int().min(1).max(10).optional().describe("Max results (default 5)"), }, handleTool(async ({ id, limit }) => { const data = await client.get(`/api/news/${id}/similar`, { limit }); 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(`/api/news/${id}/check-duplicates`); return JSON.stringify(data, null, 2); }), ); server.tool( "create_news_item", "Create a new draft news item. Requires news:write scope.", { title: z.string().min(1).max(200).describe("Article title"), readMoreUrl: z.string().url().describe("URL to the original article"), sourceId: z.string().uuid().describe("UUID of the source this article belongs to"), summary: z.string().max(1000).optional().describe("Short summary of the article"), longDescription: z.string().optional().describe("Detailed description"), authorsThought: z.string().max(500).optional().describe("Author commentary"), category: z.string().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().optional().describe("Hero image URL"), imageAltText: z.string().optional().describe("Image alt text"), imageCredits: z.string().optional().describe("Image attribution"), datePublished: z.string().datetime().optional().describe("Original publication date (ISO 8601)"), }, handleTool(async (args) => { const data = await client.post("/api/news", args); return JSON.stringify(data, null, 2); }), ); server.tool( "submit_news_item", "Submit a draft news item for moderation review. Requires news:write scope and ownership.", { id: z.string().uuid().describe("News item UUID"), }, handleTool(async ({ id }) => { const data = await client.post(`/api/news/${id}/submit`); return JSON.stringify(data, null, 2); }), ); server.tool( "update_news_item", "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().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)"), }, handleTool(async ({ id, ...body }) => { const data = await client.patch(`/api/news/${id}`, body); return JSON.stringify(data, null, 2); }), ); server.tool( "review_news_item", "Approve, reject, or request revision on a news item. Requires Mod/Admin role and news:moderate scope.", { id: z.string().uuid().describe("News item UUID"), action: z.enum(["approve", "reject", "needs_revision"]).describe("Review decision"), comment: z.string().optional().describe("Optional feedback comment"), }, handleTool(async ({ id, ...body }) => { const data = await client.post(`/api/news/${id}/review`, body); return JSON.stringify(data, null, 2); }), ); server.tool( "like_news_item", "Toggle a like on a news item. Requires news:write scope.", { id: z.string().uuid().describe("News item UUID"), }, handleTool(async ({ id }) => { const data = await client.post(`/api/news/${id}/like`); return JSON.stringify(data, null, 2); }), ); server.tool( "dislike_news_item", "Toggle a dislike on a news item. Requires news:write scope.", { id: z.string().uuid().describe("News item UUID"), }, handleTool(async ({ id }) => { const data = await client.post(`/api/news/${id}/dislike`); return JSON.stringify(data, null, 2); }), ); server.tool( "bookmark_news_item", "Toggle a bookmark on a news item. Requires bookmarks:write scope.", { id: z.string().uuid().describe("News item UUID"), }, handleTool(async ({ id }) => { const data = await client.post(`/api/news/${id}/bookmark`); return JSON.stringify(data, null, 2); }), ); server.tool( "delete_news_item", "Delete a news item. Owners can delete their own drafts; Admins can delete anything. Requires news:write scope.", { id: z.string().uuid().describe("News item UUID"), }, handleTool(async ({ id }) => { const data = await client.delete(`/api/news/${id}`); return JSON.stringify(data, null, 2); }), ); server.tool( "get_news_item_stats", "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"), }, handleTool(async ({ id }) => { const data = await client.get(`/api/news/${id}/stats`); return JSON.stringify(data, null, 2); }), ); }