From fa43346e513eeffe4daf5e5c643349d7c69e5aa8 Mon Sep 17 00:00:00 2001 From: Space-Banane Date: Thu, 18 Jun 2026 16:49:16 +0200 Subject: [PATCH] feat: update version to 1.1.0 and add new tools for blog, API keys, and sessions --- package.json | 2 +- src/index.ts | 8 ++- src/tools/api-keys.ts | 98 +++++++++++++++++++++++++++++ src/tools/blog.ts | 99 ++++++++++++++++++++++++++++++ src/tools/comments.ts | 40 ++++++++++++ src/tools/moderation.ts | 18 ++++++ src/tools/news.ts | 133 ++++++++++++++++++++++++++++++++++------ src/tools/sessions.ts | 38 ++++++++++++ src/tools/sources.ts | 26 ++++---- src/tools/tickets.ts | 4 +- src/tools/user.ts | 17 +++++ 11 files changed, 448 insertions(+), 35 deletions(-) create mode 100644 src/tools/api-keys.ts create mode 100644 src/tools/blog.ts create mode 100644 src/tools/sessions.ts diff --git a/package.json b/package.json index 3e5e9f0..a0d75b8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "betternews-mcp", - "version": "1.0.0", + "version": "1.1.0", "description": "MCP server for the BetterNews API", "type": "module", "main": "dist/index.js", diff --git a/src/index.ts b/src/index.ts index 9c70af7..83141a6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,9 @@ import { registerUserTools } from "./tools/user.js"; import { registerModerationTools } from "./tools/moderation.js"; import { registerTicketTools } from "./tools/tickets.js"; import { registerLegalTools } from "./tools/legal.js"; +import { registerBlogTools } from "./tools/blog.js"; +import { registerApiKeyTools } from "./tools/api-keys.js"; +import { registerSessionTools } from "./tools/sessions.js"; const apiKey = process.env.BETTERNEWS_API_KEY; if (!apiKey) { @@ -24,7 +27,7 @@ const client = new BetterNewsClient(apiKey, baseUrl); const server = new McpServer({ name: "betternews", - version: "1.0.0", + version: "1.1.0", }); registerNewsTools(server, client); @@ -34,6 +37,9 @@ registerUserTools(server, client); registerModerationTools(server, client); registerTicketTools(server, client); registerLegalTools(server, client); +registerBlogTools(server, client); +registerApiKeyTools(server, client); +registerSessionTools(server, client); const transport = new StdioServerTransport(); await server.connect(transport); diff --git a/src/tools/api-keys.ts b/src/tools/api-keys.ts new file mode 100644 index 0000000..b44c0ed --- /dev/null +++ b/src/tools/api-keys.ts @@ -0,0 +1,98 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import type { BetterNewsClient } from "../client.js"; +import { handleTool } from "../utils.js"; + +const SCOPES = [ + "news:read", + "news:write", + "news:moderate", + "news:comment", + "sources:read", + "sources:write", + "bookmarks:write", + "tickets:read", + "tickets:write", + "tickets:moderate", + "mod:queue", + "mod:stats", + "mod:users", + "blog:write", + "legal:write", + "user:profile", + "user:apikeys", + "privacy:read", + "files:write", +] as const; + +export function registerApiKeyTools(server: McpServer, client: BetterNewsClient) { + server.tool( + "list_api_keys", + "List API keys for the authenticated user. Requires user:apikeys scope.", + {}, + handleTool(async () => { + const data = await client.get("/api/user/api-keys"); + return JSON.stringify(data, null, 2); + }), + ); + + server.tool( + "create_api_key", + "Create a new API key. The raw key value is returned only once. Requires cookie-based session (not another API key) and user:apikeys scope.", + { + name: z.string().min(1).max(50).describe("Human-readable name for this key"), + scopes: z + .array(z.enum(SCOPES)) + .optional() + .describe("Permission scopes. Empty array = full access (inherits your role)."), + expiresAt: z + .string() + .datetime() + .nullable() + .optional() + .describe("Expiry date (ISO 8601). Null = never expires."), + }, + handleTool(async (body) => { + const data = await client.post("/api/user/api-keys", body); + return JSON.stringify(data, null, 2); + }), + ); + + server.tool( + "update_api_key", + "Update an API key's name, scopes, or expiry. Requires user:apikeys scope.", + { + keyId: z.string().uuid().describe("API key UUID"), + name: z.string().min(1).max(50).optional().describe("New name"), + scopes: z.array(z.enum(SCOPES)).optional().describe("New scopes list"), + expiresAt: z + .string() + .datetime() + .nullable() + .optional() + .describe("New expiry date (ISO 8601). Null = never expires."), + }, + handleTool(async ({ keyId, ...body }) => { + const data = await client.patch(`/api/user/api-keys/${keyId}`, body); + return JSON.stringify(data, null, 2); + }), + ); + + server.tool( + "revoke_api_key", + "Revoke or permanently delete an API key. Pass hardDelete=true to permanently remove it; otherwise it is soft-revoked and stays visible in the list. Requires user:apikeys scope.", + { + keyId: z.string().uuid().describe("API key UUID"), + hardDelete: z + .boolean() + .optional() + .describe("True to permanently delete; false (default) to soft-revoke"), + }, + handleTool(async ({ keyId, hardDelete }) => { + const data = await client.delete( + `/api/user/api-keys/${keyId}${hardDelete ? "?hardDelete=true" : ""}`, + ); + return JSON.stringify(data, null, 2); + }), + ); +} diff --git a/src/tools/blog.ts b/src/tools/blog.ts new file mode 100644 index 0000000..5429303 --- /dev/null +++ b/src/tools/blog.ts @@ -0,0 +1,99 @@ +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 registerBlogTools(server: McpServer, client: BetterNewsClient) { + server.tool( + "list_blog_posts", + "List blog posts. Public sees published only; Mod/Admin see all statuses. No auth required for published posts.", + { + 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(["draft", "published"]).optional().describe("Filter by status (Mod/Admin only for draft)"), + }, + handleTool(async (args) => { + const data = await client.get("/api/blog", args); + return JSON.stringify(data, null, 2); + }), + ); + + server.tool( + "get_blog_post", + "Get a single blog post by slug. Published posts are publicly accessible; drafts require Mod/Admin role.", + { + slug: z.string().describe("Blog post slug"), + }, + handleTool(async ({ slug }) => { + const data = await client.get(`/api/blog/${slug}`); + return JSON.stringify(data, null, 2); + }), + ); + + server.tool( + "create_blog_post", + "Create a new blog post draft. Requires Mod/Admin role and blog:write scope.", + { + title: z.string().min(1).max(150).describe("Post title"), + content: z.string().min(1).describe("Post body (markdown supported)"), + excerpt: z.string().max(300).optional().describe("Short excerpt shown in listings"), + imageUrl: z.string().url().optional().describe("Hero image URL"), + }, + handleTool(async (body) => { + const data = await client.post("/api/blog", body); + return JSON.stringify(data, null, 2); + }), + ); + + server.tool( + "update_blog_post", + "Update a blog post. Requires Mod/Admin role and blog:write scope.", + { + id: z.string().uuid().describe("Blog post UUID"), + title: z.string().min(1).max(150).optional().describe("Post title"), + content: z.string().min(1).optional().describe("Post body"), + excerpt: z.string().max(300).nullable().optional().describe("Short excerpt"), + imageUrl: z.string().url().nullable().optional().describe("Hero image URL"), + }, + handleTool(async ({ id, ...body }) => { + const data = await client.patch(`/api/blog/${id}`, body); + return JSON.stringify(data, null, 2); + }), + ); + + server.tool( + "delete_blog_post", + "Delete a blog post. Requires Mod/Admin role and blog:write scope.", + { + id: z.string().uuid().describe("Blog post UUID"), + }, + handleTool(async ({ id }) => { + const data = await client.delete(`/api/blog/${id}`); + return JSON.stringify(data, null, 2); + }), + ); + + server.tool( + "publish_blog_post", + "Publish a draft blog post, making it publicly visible. Requires Mod/Admin role and blog:write scope.", + { + id: z.string().uuid().describe("Blog post UUID"), + }, + handleTool(async ({ id }) => { + const data = await client.post(`/api/blog/${id}/publish`); + return JSON.stringify(data, null, 2); + }), + ); + + server.tool( + "unpublish_blog_post", + "Take down a published blog post, reverting it to draft. Requires Mod/Admin role and blog:write scope.", + { + id: z.string().uuid().describe("Blog post UUID"), + }, + handleTool(async ({ id }) => { + const data = await client.post(`/api/blog/${id}/unpublish`); + return JSON.stringify(data, null, 2); + }), + ); +} diff --git a/src/tools/comments.ts b/src/tools/comments.ts index ecbd9d0..8752f9b 100644 --- a/src/tools/comments.ts +++ b/src/tools/comments.ts @@ -43,4 +43,44 @@ export function registerCommentTools(server: McpServer, client: BetterNewsClient return JSON.stringify(data, null, 2); }), ); + + server.tool( + "like_comment", + "Toggle a like on a comment. Requires news:comment scope.", + { + newsItemId: z.string().uuid().describe("News item UUID"), + commentId: z.string().uuid().describe("Comment UUID"), + }, + handleTool(async ({ newsItemId, commentId }) => { + const data = await client.post(`/api/news/${newsItemId}/comments/${commentId}/like`); + return JSON.stringify(data, null, 2); + }), + ); + + server.tool( + "dislike_comment", + "Toggle a dislike on a comment. Requires news:comment scope.", + { + newsItemId: z.string().uuid().describe("News item UUID"), + commentId: z.string().uuid().describe("Comment UUID"), + }, + handleTool(async ({ newsItemId, commentId }) => { + const data = await client.post(`/api/news/${newsItemId}/comments/${commentId}/dislike`); + return JSON.stringify(data, null, 2); + }), + ); + + server.tool( + "hide_comment", + "Hide or unhide a comment. Hidden comments are invisible to regular users but still visible to Mod/Admin. Requires Mod/Admin role and news:moderate scope.", + { + newsItemId: z.string().uuid().describe("News item UUID"), + commentId: z.string().uuid().describe("Comment UUID"), + hidden: z.boolean().describe("True to hide, false to unhide"), + }, + handleTool(async ({ newsItemId, commentId, hidden }) => { + const data = await client.patch(`/api/news/${newsItemId}/comments/${commentId}`, { hidden }); + return JSON.stringify(data, null, 2); + }), + ); } diff --git a/src/tools/moderation.ts b/src/tools/moderation.ts index 043301d..ac3888c 100644 --- a/src/tools/moderation.ts +++ b/src/tools/moderation.ts @@ -137,4 +137,22 @@ export function registerModerationTools(server: McpServer, client: BetterNewsCli return JSON.stringify(data, null, 2); }), ); + + server.tool( + "list_mod_comments", + "List all comments across the platform with moderation details (like/dislike counts, hidden status). Requires Mod/Admin role and mod:queue 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)"), + hidden: z.boolean().optional().describe("Filter by hidden status. Omit to see all."), + sort: z + .enum(["top", "most_disliked", "new", "oldest"]) + .optional() + .describe("Sort order: top = most liked, most_disliked, new (default), oldest"), + }, + handleTool(async (args) => { + const data = await client.get("/api/mod/comments", args); + return JSON.stringify(data, null, 2); + }), + ); } diff --git a/src/tools/news.ts b/src/tools/news.ts index 772ea5a..1b61e11 100644 --- a/src/tools/news.ts +++ b/src/tools/news.ts @@ -19,7 +19,7 @@ export function registerNewsTools(server: McpServer, client: BetterNewsClient) { "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)"), + limit: z.number().int().min(1).max(50).optional().describe("Items per page (default 20, max 50)"), 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"), @@ -89,19 +89,19 @@ export function registerNewsTools(server: McpServer, client: BetterNewsClient) { "create_news_item", "Create a new draft news item. Requires news:write scope.", { - title: z.string().min(1).max(200).describe("Article title"), + title: z.string().min(3).max(150).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"), + summary: z.string().max(200).optional().describe("Short summary of the article"), + longDescription: z.string().max(1000).optional().describe("Detailed description"), + authorsThought: z.string().max(250).optional().describe("Author commentary"), + category: z.string().min(1).max(100).describe("Category slug"), + tags: z.array(z.string().max(50)).max(3).optional().describe("Up to 3 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"), + imageAltText: z.string().max(200).optional().describe("Image alt text"), + imageCredits: z.string().max(200).optional().describe("Image attribution"), datePublished: z.string().datetime().optional().describe("Original publication date (ISO 8601)"), }, handleTool(async (args) => { @@ -127,19 +127,19 @@ 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().describe("Article title"), + title: z.string().min(3).max(150).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"), + summary: z.string().max(200).optional().describe("Short summary"), + longDescription: z.string().max(1000).optional().describe("Detailed description"), + authorsThought: z.string().max(250).optional().describe("Author commentary"), + category: z.string().min(1).max(100).optional().describe("Category slug"), + tags: z.array(z.string().max(50)).max(3).optional().describe("Up to 3 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"), + imageUrl: z.string().nullable().optional().describe("Hero image URL"), + imageAltText: z.string().max(200).nullable().optional().describe("Image alt text"), + imageCredits: z.string().max(200).nullable().optional().describe("Image attribution"), datePublished: z.string().datetime().nullable().optional().describe("Original publication date (ISO 8601)"), }, handleTool(async ({ id, ...body }) => { @@ -221,4 +221,101 @@ export function registerNewsTools(server: McpServer, client: BetterNewsClient) { return JSON.stringify(data, null, 2); }), ); + + server.tool( + "list_topics", + "List all unique topic tags from published news items. Results are cached for 1 hour. No auth required.", + {}, + handleTool(async () => { + const data = await client.get("/api/news/topics"); + return JSON.stringify(data, null, 2); + }), + ); + + server.tool( + "publish_news_item", + "Publish an approved news item, making it live on the feed. The item must be in the 'approved' state. Owner or Mod/Admin may call this. Requires news:moderate scope.", + { + id: z.string().uuid().describe("News item UUID"), + }, + handleTool(async ({ id }) => { + const data = await client.post(`/api/news/${id}/publish`); + return JSON.stringify(data, null, 2); + }), + ); + + server.tool( + "unpublish_news_item", + "Take down a published news item, archiving it. Owner or Mod/Admin may call this. Requires news:moderate scope.", + { + id: z.string().uuid().describe("News item UUID"), + }, + handleTool(async ({ id }) => { + const data = await client.post(`/api/news/${id}/unpublish`); + return JSON.stringify(data, null, 2); + }), + ); + + server.tool( + "republish_news_item", + "Re-publish an archived news item, putting it back on the live feed. Requires Mod/Admin role and news:moderate scope.", + { + id: z.string().uuid().describe("News item UUID"), + }, + handleTool(async ({ id }) => { + const data = await client.post(`/api/news/${id}/republish`); + return JSON.stringify(data, null, 2); + }), + ); + + server.tool( + "archive_news_item", + "Move a rejected news item to archived status so the owner can edit and resubmit it. Owner only. Requires news:moderate scope.", + { + id: z.string().uuid().describe("News item UUID"), + }, + handleTool(async ({ id }) => { + const data = await client.post(`/api/news/${id}/archive`); + return JSON.stringify(data, null, 2); + }), + ); + + server.tool( + "direct_publish_news_item", + "Publish a news item directly to the feed, bypassing the moderation queue. Any existing status is overridden. Requires Mod/Admin role and news:moderate scope.", + { + id: z.string().uuid().describe("News item UUID"), + }, + handleTool(async ({ id }) => { + const data = await client.post(`/api/news/${id}/direct-publish`); + return JSON.stringify(data, null, 2); + }), + ); + + server.tool( + "direct_create_publish_news_item", + "Create a news item and publish it immediately in one step, bypassing the moderation queue. Admin only. Requires news:moderate scope.", + { + title: z.string().min(3).max(150).describe("Article title"), + readMoreUrl: z.string().url().describe("URL to the original article"), + category: z.string().min(1).max(100).describe("Category slug"), + sourceId: z.string().uuid().optional().describe("UUID of the source"), + summary: z.string().max(200).optional().describe("Short summary"), + longDescription: z.string().max(1000).optional().describe("Detailed description"), + tags: z.array(z.string().max(50)).max(3).optional().describe("Up to 3 tag strings"), + imageUrl: z.string().url().optional().describe("Hero image URL"), + imageAltText: z.string().max(200).optional().describe("Image alt text"), + imageCredits: z.string().max(200).optional().describe("Image attribution"), + readTime: z.number().int().positive().optional().describe("Estimated read time in minutes"), + authorsThought: z.string().max(250).optional().describe("Author commentary"), + language: z.string().max(10).optional().describe("ISO 639-1 language code"), + country: z.string().max(10).optional().describe("ISO 3166-1 alpha-2 country code"), + datePublished: z.string().datetime().optional().describe("Original publication date (ISO 8601)"), + createdAt: z.string().datetime().optional().describe("Override creation timestamp (ISO 8601)"), + }, + handleTool(async (body) => { + const data = await client.post("/api/news/direct", body); + return JSON.stringify(data, null, 2); + }), + ); } diff --git a/src/tools/sessions.ts b/src/tools/sessions.ts new file mode 100644 index 0000000..8f62c92 --- /dev/null +++ b/src/tools/sessions.ts @@ -0,0 +1,38 @@ +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 registerSessionTools(server: McpServer, client: BetterNewsClient) { + server.tool( + "list_sessions", + "List all active sessions for the authenticated user. The current session is flagged with `current: true`.", + {}, + handleTool(async () => { + const data = await client.get("/api/user/sessions"); + return JSON.stringify(data, null, 2); + }), + ); + + server.tool( + "revoke_all_sessions", + "Revoke all sessions for the authenticated user, forcing re-login everywhere. The current session is also invalidated.", + {}, + handleTool(async () => { + const data = await client.delete("/api/user/sessions"); + return JSON.stringify(data, null, 2); + }), + ); + + server.tool( + "revoke_session", + "Revoke a single session by ID. Use list_sessions to find session IDs.", + { + sessionId: z.string().uuid().describe("Session UUID to revoke"), + }, + handleTool(async ({ sessionId }) => { + const data = await client.delete(`/api/user/sessions/${sessionId}`); + return JSON.stringify(data, null, 2); + }), + ); +} diff --git a/src/tools/sources.ts b/src/tools/sources.ts index 7c2aed7..d4443ce 100644 --- a/src/tools/sources.ts +++ b/src/tools/sources.ts @@ -9,7 +9,7 @@ export function registerSourceTools(server: McpServer, client: BetterNewsClient) "List curated news sources. Requires sources:read scope.", { 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)"), + limit: z.number().int().min(1).max(50).optional().describe("Items per page (default 20, max 50)"), search: z.string().optional().describe("Text search over source name"), type: z.string().optional().describe("Filter by source type"), }, @@ -35,13 +35,13 @@ export function registerSourceTools(server: McpServer, client: BetterNewsClient) "create_source", "Create a new news source. Requires Mod/Admin role and sources:write scope.", { - name: z.string().min(1).max(100).describe("Source name"), - type: z.string().describe("Source type (e.g. newspaper, blog, wire)"), + name: z.string().min(1).max(200).describe("Source name"), + type: z.string().min(1).max(50).describe("Source type (e.g. newspaper, blog, wire)"), url: z.string().url().optional().describe("Source homepage URL"), - description: z.string().optional().describe("Short description"), - credibilityScore: z.number().min(0).max(100).optional().describe("Credibility score 0–100"), - country: z.string().optional().describe("ISO 3166-1 alpha-2 country code"), - language: z.string().optional().describe("ISO 639-1 language code"), + description: z.string().max(1000).optional().describe("Short description"), + credibilityScore: z.number().min(0).max(10).optional().describe("Credibility score 0–10"), + country: z.string().max(10).optional().describe("ISO 3166-1 alpha-2 country code"), + language: z.string().max(10).optional().describe("ISO 639-1 language code"), contactEmail: z.string().email().optional().describe("Contact email"), logoUrl: z.string().url().optional().describe("Logo image URL"), }, @@ -56,13 +56,13 @@ export function registerSourceTools(server: McpServer, client: BetterNewsClient) "Update a news source. Requires Mod/Admin role and sources:write scope.", { id: z.string().uuid().describe("Source UUID"), - name: z.string().min(1).max(100).optional().describe("Source name"), - type: z.string().optional().describe("Source type (e.g. newspaper, blog, wire)"), + name: z.string().min(1).max(200).optional().describe("Source name"), + type: z.string().min(1).max(50).optional().describe("Source type (e.g. newspaper, blog, wire)"), url: z.string().url().nullable().optional().describe("Source homepage URL"), - description: z.string().nullable().optional().describe("Short description"), - credibilityScore: z.number().min(0).max(100).nullable().optional().describe("Credibility score 0–100"), - country: z.string().nullable().optional().describe("ISO 3166-1 alpha-2 country code"), - language: z.string().nullable().optional().describe("ISO 639-1 language code"), + description: z.string().max(1000).nullable().optional().describe("Short description"), + credibilityScore: z.number().min(0).max(10).nullable().optional().describe("Credibility score 0–10"), + country: z.string().max(10).nullable().optional().describe("ISO 3166-1 alpha-2 country code"), + language: z.string().max(10).nullable().optional().describe("ISO 639-1 language code"), contactEmail: z.string().email().nullable().optional().describe("Contact email"), logoUrl: z.string().url().nullable().optional().describe("Logo image URL"), hidden: z.boolean().optional().describe("Hide/unhide this source"), diff --git a/src/tools/tickets.ts b/src/tools/tickets.ts index 8c62bb0..05ab5d7 100644 --- a/src/tools/tickets.ts +++ b/src/tools/tickets.ts @@ -34,8 +34,8 @@ export function registerTicketTools(server: McpServer, client: BetterNewsClient) "create_ticket", "Open a new support ticket. Requires tickets:write scope.", { - title: z.string().min(1).max(200).describe("Ticket title"), - description: z.string().min(1).max(5000).describe("Detailed description of the issue"), + title: z.string().min(3).max(100).describe("Ticket title"), + description: z.string().min(10).max(5000).describe("Detailed description of the issue"), priority: z.enum(["low", "medium", "high", "critical"]).describe("Ticket priority"), }, handleTool(async (body) => { diff --git a/src/tools/user.ts b/src/tools/user.ts index 54a26c1..0cbef15 100644 --- a/src/tools/user.ts +++ b/src/tools/user.ts @@ -84,4 +84,21 @@ export function registerUserTools(server: McpServer, client: BetterNewsClient) { return JSON.stringify(data, null, 2); }), ); + + server.tool( + "update_privacy_settings", + "Update the authenticated user's privacy settings. Requires user:profile scope.", + { + profilePublic: z.boolean().optional().describe("Whether the profile is publicly visible"), + showEmail: z.boolean().optional().describe("Whether to show email address on public profile"), + autoPublishAfterReview: z + .boolean() + .optional() + .describe("Automatically publish news items as soon as they are approved by a moderator"), + }, + handleTool(async (body) => { + const data = await client.patch("/api/user/privacy", body); + return JSON.stringify(data, null, 2); + }), + ); }