feat: initialize BetterNews MCP server with core functionality

- Add .gitignore to exclude node_modules, dist, and .env files
- Create README.md with setup instructions and API key information
- Add package.json with dependencies, scripts, and project metadata
- Implement BetterNewsClient for API interactions
- Set up index.ts to initialize MCP server and register tools
- Create tools for managing news, comments, sources, users, moderation, and tickets
- Add TypeScript configuration for project compilation
This commit is contained in:
Space-Banane
2026-06-17 21:30:52 +02:00
commit 6c0a079808
13 changed files with 1005 additions and 0 deletions
+45
View File
@@ -0,0 +1,45 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import type { BetterNewsClient } from "../client.js";
export function registerCommentTools(server: McpServer, client: BetterNewsClient) {
server.tool(
"list_comments",
"List comments on a news item. Moderators also see hidden comments. Requires news:read scope.",
{
newsItemId: z.string().uuid().describe("News item UUID"),
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)"),
},
async ({ newsItemId, page, limit }) => {
const data = await client.get<unknown>(`/api/news/${newsItemId}/comments`, { page, limit });
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
);
server.tool(
"create_comment",
"Post a comment on a published news item. Requires news:comment scope.",
{
newsItemId: z.string().uuid().describe("News item UUID"),
content: z.string().min(1).max(2000).describe("Comment text"),
},
async ({ newsItemId, content }) => {
const data = await client.post<unknown>(`/api/news/${newsItemId}/comments`, { content });
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
);
server.tool(
"delete_comment",
"Delete a comment. Owners can delete their own; Mod/Admin can delete any. Requires news:comment scope.",
{
newsItemId: z.string().uuid().describe("News item UUID"),
commentId: z.string().uuid().describe("Comment UUID"),
},
async ({ newsItemId, commentId }) => {
const data = await client.delete<unknown>(`/api/news/${newsItemId}/comments/${commentId}`);
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
);
}
+50
View File
@@ -0,0 +1,50 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import type { BetterNewsClient } from "../client.js";
export function registerLegalTools(server: McpServer, client: BetterNewsClient) {
server.tool(
"get_terms_of_service",
"Get the current published Terms of Service document.",
{},
async () => {
const data = await client.get<unknown>("/api/legal/current/terms_of_service");
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
);
server.tool(
"get_privacy_policy",
"Get the current published Privacy Policy document.",
{},
async () => {
const data = await client.get<unknown>("/api/legal/current/privacy_policy");
return { content: [{ type: "text", text: 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(),
limit: z.number().int().min(1).max(50).optional(),
},
async (args) => {
const data = await client.get<unknown>("/api/legal", args);
return { content: [{ type: "text", text: 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.",
{},
async () => {
const data = await client.post<unknown>("/api/legal/accept");
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
);
}
+110
View File
@@ -0,0 +1,110 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import type { BetterNewsClient } from "../client.js";
export function registerModerationTools(server: McpServer, client: BetterNewsClient) {
server.tool(
"get_mod_queue",
"Get the moderation queue of pending news items and source submissions. 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)"),
},
async (args) => {
const data = await client.get<unknown>("/api/mod/queue", args);
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
);
server.tool(
"get_mod_stats",
"Get aggregate moderation statistics. Requires Mod/Admin role and mod:stats scope.",
{},
async () => {
const data = await client.get<unknown>("/api/mod/stats");
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
);
server.tool(
"list_mod_users",
"List all users with moderation details. Requires Admin role and mod:users 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)"),
search: z.string().optional().describe("Search by username or email"),
role: z.enum(["User", "Moderator", "Admin"]).optional().describe("Filter by role"),
banned: z.boolean().optional().describe("Filter by ban status"),
},
async (args) => {
const data = await client.get<unknown>("/api/mod/users", args);
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
);
server.tool(
"get_mod_user",
"Get detailed moderation view of a specific user. Requires Admin role and mod:users scope.",
{
userId: z.string().uuid().describe("User UUID"),
},
async ({ userId }) => {
const data = await client.get<unknown>(`/api/mod/users/${userId}`);
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
);
server.tool(
"change_user_role",
"Change a user's role. Requires Admin role and mod:users scope.",
{
userId: z.string().uuid().describe("User UUID"),
role: z.enum(["User", "Moderator", "Admin"]).describe("New role"),
},
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) }] };
},
);
server.tool(
"ban_user",
"Ban a user. Requires Admin role and mod:users scope.",
{
userId: z.string().uuid().describe("User UUID"),
reason: z.string().max(500).optional().describe("Ban reason"),
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 }) => {
const data = await client.post<unknown>(`/api/mod/users/${userId}/ban`, body);
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
);
server.tool(
"unban_user",
"Lift a ban from a user. Requires Admin role and mod:users scope.",
{
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) }] };
},
);
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(),
},
async (args) => {
const data = await client.get<unknown>("/api/mod/tickets", args);
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
);
}
+211
View File
@@ -0,0 +1,211 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import type { BetterNewsClient } from "../client.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 () => {
const data = await client.get<unknown>("/api/news/top");
return { content: [{ type: "text", text: 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"),
},
async (args) => {
const data = await client.get<unknown>("/api/news", args);
return { content: [{ type: "text", text: 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"),
},
async ({ id }) => {
const data = await client.get<unknown>(`/api/news/${id}`);
return { content: [{ type: "text", text: 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)"),
},
async ({ q, limit }) => {
const data = await client.get<unknown>("/api/news/search", { q, limit });
return { content: [{ type: "text", text: 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)"),
},
async ({ id, limit }) => {
const data = await client.get<unknown>(`/api/news/${id}/similar`, { limit });
return { content: [{ type: "text", text: 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)"),
},
async (args) => {
const data = await client.post<unknown>("/api/news", args);
return { content: [{ type: "text", text: 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"),
},
async ({ id }) => {
const data = await client.post<unknown>(`/api/news/${id}/submit`);
return { content: [{ type: "text", text: 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(),
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(),
},
async ({ id, ...body }) => {
const data = await client.patch<unknown>(`/api/news/${id}`, body);
return { content: [{ type: "text", text: 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"),
},
async ({ id, ...body }) => {
const data = await client.post<unknown>(`/api/news/${id}/review`, body);
return { content: [{ type: "text", text: 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"),
},
async ({ id }) => {
const data = await client.post<unknown>(`/api/news/${id}/like`);
return { content: [{ type: "text", text: 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"),
},
async ({ id }) => {
const data = await client.post<unknown>(`/api/news/${id}/dislike`);
return { content: [{ type: "text", text: 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"),
},
async ({ id }) => {
const data = await client.post<unknown>(`/api/news/${id}/bookmark`);
return { content: [{ type: "text", text: 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"),
},
async ({ id }) => {
const data = await client.delete<unknown>(`/api/news/${id}`);
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
);
server.tool(
"get_news_item_stats",
"Get engagement statistics for a news item. Requires Mod/Admin role.",
{
id: z.string().uuid().describe("News item UUID"),
},
async ({ id }) => {
const data = await client.get<unknown>(`/api/news/${id}/stats`);
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
);
}
+119
View File
@@ -0,0 +1,119 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import type { BetterNewsClient } from "../client.js";
export function registerSourceTools(server: McpServer, client: BetterNewsClient) {
server.tool(
"list_sources",
"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)"),
search: z.string().optional().describe("Text search over source name"),
type: z.string().optional().describe("Filter by source type"),
},
async (args) => {
const data = await client.get<unknown>("/api/sources", args);
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
);
server.tool(
"get_source",
"Get a single news source by ID. Requires sources:read scope.",
{
id: z.string().uuid().describe("Source UUID"),
},
async ({ id }) => {
const data = await client.get<unknown>(`/api/sources/${id}`);
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
);
server.tool(
"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)"),
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 0100"),
country: z.string().optional().describe("ISO 3166-1 alpha-2 country code"),
language: z.string().optional().describe("ISO 639-1 language code"),
contactEmail: z.string().email().optional().describe("Contact email"),
logoUrl: z.string().url().optional().describe("Logo image URL"),
},
async (args) => {
const data = await client.post<unknown>("/api/sources", args);
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
);
server.tool(
"update_source",
"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(),
type: z.string().optional(),
url: z.string().url().nullable().optional(),
description: z.string().nullable().optional(),
credibilityScore: z.number().min(0).max(100).nullable().optional(),
country: z.string().nullable().optional(),
language: z.string().nullable().optional(),
contactEmail: z.string().email().nullable().optional(),
logoUrl: z.string().url().nullable().optional(),
hidden: z.boolean().optional().describe("Hide/unhide this source"),
},
async ({ id, ...body }) => {
const data = await client.patch<unknown>(`/api/sources/${id}`, body);
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
);
server.tool(
"list_source_submissions",
"List community source submissions on a news item. Requires news:read scope.",
{
newsItemId: z.string().uuid().describe("News item UUID"),
},
async ({ newsItemId }) => {
const data = await client.get<unknown>(`/api/news/${newsItemId}/sources`);
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
);
server.tool(
"submit_source",
"Submit a source suggestion for a news item. Requires news:write scope.",
{
newsItemId: z.string().uuid().describe("News item UUID"),
readMoreUrl: z.string().url().describe("URL to the source article"),
title: z.string().optional().describe("Optional title for the source link"),
description: z.string().optional().describe("Optional description"),
},
async ({ newsItemId, ...body }) => {
const data = await client.post<unknown>(`/api/news/${newsItemId}/sources`, body);
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
);
server.tool(
"review_source_submission",
"Approve or reject a community source submission. Requires Mod/Admin role and news:moderate scope.",
{
newsItemId: z.string().uuid().describe("News item UUID"),
submissionId: z.string().uuid().describe("Source submission UUID"),
action: z.enum(["approve", "reject"]).describe("Review decision"),
comment: z.string().optional().describe("Optional feedback"),
},
async ({ newsItemId, submissionId, ...body }) => {
const data = await client.post<unknown>(
`/api/news/${newsItemId}/sources/${submissionId}/review`,
body,
);
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
);
}
+71
View File
@@ -0,0 +1,71 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import type { BetterNewsClient } from "../client.js";
export function registerTicketTools(server: McpServer, client: BetterNewsClient) {
server.tool(
"list_my_tickets",
"List the authenticated user's support tickets. Requires tickets:read scope.",
{
page: z.number().int().positive().optional(),
limit: z.number().int().min(1).max(50).optional(),
status: z.enum(["open", "in_progress", "resolved", "closed"]).optional(),
},
async (args) => {
const data = await client.get<unknown>("/api/tickets", args);
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
);
server.tool(
"get_ticket",
"Get a support ticket with its reply history. Requires ownership or Mod/Admin role. Requires tickets:read scope.",
{
id: z.string().uuid().describe("Ticket UUID"),
},
async ({ id }) => {
const data = await client.get<unknown>(`/api/tickets/${id}`);
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
);
server.tool(
"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"),
priority: z.enum(["low", "medium", "high", "critical"]).describe("Ticket priority"),
},
async (body) => {
const data = await client.post<unknown>("/api/tickets", body);
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
);
server.tool(
"reply_to_ticket",
"Add a reply to a support ticket. Owners can reply to their own tickets; Mod/Admin can reply to any. Requires tickets:write scope.",
{
id: z.string().uuid().describe("Ticket UUID"),
content: z.string().min(1).max(5000).describe("Reply text"),
},
async ({ id, content }) => {
const data = await client.post<unknown>(`/api/tickets/${id}/reply`, { content });
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
);
server.tool(
"update_ticket_status",
"Update the status of a support ticket. Requires Mod/Admin role and tickets:moderate scope.",
{
id: z.string().uuid().describe("Ticket UUID"),
status: z.enum(["open", "in_progress", "resolved", "closed"]).describe("New status"),
},
async ({ id, status }) => {
const data = await client.patch<unknown>(`/api/tickets/${id}/status`, { status });
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
);
}
+86
View File
@@ -0,0 +1,86 @@
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import type { BetterNewsClient } from "../client.js";
export function registerUserTools(server: McpServer, client: BetterNewsClient) {
server.tool(
"get_current_user",
"Get the authenticated user's profile and account details. Requires user:profile scope.",
{},
async () => {
const data = await client.get<unknown>("/api/user/whoami");
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
);
server.tool(
"get_user_profile",
"Get a public user profile by username.",
{
username: z.string().describe("Username to look up"),
},
async ({ username }) => {
const data = await client.get<unknown>(`/api/user/profile/${username}`);
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
);
server.tool(
"list_my_news",
"List the authenticated user's own news items. Requires user:profile 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)"),
status: z
.enum(["published", "draft", "pending_review", "approved", "rejected", "needs_revision", "archived"])
.optional()
.describe("Filter by status"),
},
async (args) => {
const data = await client.get<unknown>("/api/user/news", args);
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
);
server.tool(
"list_my_source_submissions",
"List the authenticated user's own source submissions. Requires user:profile scope.",
{},
async () => {
const data = await client.get<unknown>("/api/user/source-submissions");
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
);
server.tool(
"list_bookmarks",
"List the authenticated user's bookmarked news items. Requires bookmarks:write 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)"),
},
async (args) => {
const data = await client.get<unknown>("/api/user/bookmarks", args);
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
);
server.tool(
"update_profile",
"Update the authenticated user's display name or email. Requires user:profile scope.",
{
displayName: z.string().min(1).max(25).optional().describe("New display name"),
email: z.string().email().optional().describe("New email address (triggers verification)"),
bio: z.string().max(500).nullable().optional().describe("Profile bio"),
avatarUrl: z.string().url().nullable().optional().describe("Avatar image URL"),
profilePublic: z.boolean().optional().describe("Whether the profile is public"),
showEmail: z.boolean().optional().describe("Whether to show email on profile"),
emailNewsUpdates: z.boolean().optional(),
emailCommentEvents: z.boolean().optional(),
},
async (body) => {
const data = await client.patch<unknown>("/api/user/profile", body);
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
},
);
}