feat: add discussion tools for managing discussion channels and messages
CI / Build & Test (push) Successful in 15s
CI / Build & Test (push) Successful in 15s
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
# CLAUDE.md — BetterNews MCP Server
|
||||
|
||||
Stdio MCP server (`@modelcontextprotocol/sdk`) that exposes the BetterNews API as tools.
|
||||
Auth: `BETTERNEWS_API_KEY` env var, sent as `Authorization: Bearer` on every request.
|
||||
|
||||
---
|
||||
|
||||
## Source Files
|
||||
|
||||
| File | Purpose |
|
||||
| ---- | ------- |
|
||||
| `src/index.ts` | Entry point. Imports all register functions and calls them against `server` and `client`. |
|
||||
| `src/client.ts` | `BetterNewsClient` — `get/post/patch/delete` wrappers with auth header and error handling. |
|
||||
| `src/utils.ts` | `handleTool(fn)` — wraps a handler, catches errors, returns `{ content: [{ type:"text", text }] }`. |
|
||||
| `src/tools/news.ts` | 22 news tools |
|
||||
| `src/tools/comments.ts` | 6 comment tools |
|
||||
| `src/tools/sources.ts` | 7 source tools |
|
||||
| `src/tools/user.ts` | 7 user/profile tools |
|
||||
| `src/tools/moderation.ts` | 11 moderation tools |
|
||||
| `src/tools/tickets.ts` | 5 support ticket tools |
|
||||
| `src/tools/legal.ts` | 4 legal document tools |
|
||||
| `src/tools/blog.ts` | 7 blog tools |
|
||||
| `src/tools/api-keys.ts` | 4 API key tools |
|
||||
| `src/tools/sessions.ts` | 3 session tools |
|
||||
| `src/tools/discussions.ts` | 13 discussion channel tools |
|
||||
|
||||
---
|
||||
|
||||
## Complete Tool Index
|
||||
|
||||
Format: `tool_name` → file → `METHOD /api/route` → minimum auth
|
||||
|
||||
### News — `tools/news.ts`
|
||||
|
||||
| Tool | Route | Auth |
|
||||
| ---- | ----- | ---- |
|
||||
| `get_top_news` | `GET /api/news/top` | public |
|
||||
| `list_news` | `GET /api/news` | optional |
|
||||
| `get_news_item` | `GET /api/news/{id}` | optional |
|
||||
| `search_news` | `GET /api/news/search` | optional |
|
||||
| `get_similar_news` | `GET /api/news/{id}/similar` | optional |
|
||||
| `list_topics` | `GET /api/news/topics` | public |
|
||||
| `get_news_item_stats` | `GET /api/news/{id}/stats` | owner or Mod/Admin |
|
||||
| `check_duplicates` | `GET /api/news/{id}/check-duplicates` | `news:moderate` |
|
||||
| `create_news_item` | `POST /api/news` | `news:write` |
|
||||
| `update_news_item` | `PATCH /api/news/{id}` | `news:write` |
|
||||
| `delete_news_item` | `DELETE /api/news/{id}` | owner or Admin |
|
||||
| `submit_news_item` | `POST /api/news/{id}/submit` | `news:write` (owner) |
|
||||
| `review_news_item` | `POST /api/news/{id}/review` | `news:moderate` |
|
||||
| `like_news_item` | `POST /api/news/{id}/like` | `news:write` |
|
||||
| `dislike_news_item` | `POST /api/news/{id}/dislike` | `news:write` |
|
||||
| `bookmark_news_item` | `POST /api/news/{id}/bookmark` | `bookmarks:write` |
|
||||
| `publish_news_item` | `POST /api/news/{id}/publish` | `news:moderate` |
|
||||
| `unpublish_news_item` | `POST /api/news/{id}/unpublish` | `news:moderate` |
|
||||
| `republish_news_item` | `POST /api/news/{id}/republish` | `news:moderate` |
|
||||
| `archive_news_item` | `POST /api/news/{id}/archive` | `news:moderate` (owner) |
|
||||
| `direct_publish_news_item` | `POST /api/news/{id}/direct-publish` | `news:moderate` |
|
||||
| `direct_create_publish_news_item` | `POST /api/news/direct` | `news:moderate` |
|
||||
|
||||
### Comments — `tools/comments.ts`
|
||||
|
||||
| Tool | Route | Auth |
|
||||
| ---- | ----- | ---- |
|
||||
| `list_comments` | `GET /api/news/{id}/comments` | optional; sort: top/most_disliked/new/oldest |
|
||||
| `create_comment` | `POST /api/news/{id}/comments` | `news:comment` + email verified |
|
||||
| `delete_comment` | `DELETE /api/news/{id}/comments/{commentId}` | owner or `news:comment` Mod/Admin |
|
||||
| `like_comment` | `POST /api/news/{id}/comments/{commentId}/like` | `news:comment` |
|
||||
| `dislike_comment` | `POST /api/news/{id}/comments/{commentId}/dislike` | `news:comment` |
|
||||
| `hide_comment` | `PATCH /api/news/{id}/comments/{commentId}` | `news:moderate` Mod/Admin |
|
||||
|
||||
### Sources — `tools/sources.ts`
|
||||
|
||||
| Tool | Route | Auth |
|
||||
| ---- | ----- | ---- |
|
||||
| `list_sources` | `GET /api/sources` | `sources:read` |
|
||||
| `get_source` | `GET /api/sources/{id}` | `sources:read` |
|
||||
| `create_source` | `POST /api/sources` | `sources:write` Mod/Admin |
|
||||
| `update_source` | `PATCH /api/sources/{id}` | `sources:write` Mod/Admin |
|
||||
| `list_source_submissions` | `GET /api/news/{id}/sources` | `news:read` |
|
||||
| `submit_source` | `POST /api/news/{id}/sources` | `news:write` |
|
||||
| `review_source_submission` | `POST /api/news/{id}/sources/{submissionId}/review` | `news:moderate` |
|
||||
|
||||
### User — `tools/user.ts`
|
||||
|
||||
| Tool | Route | Auth |
|
||||
| ---- | ----- | ---- |
|
||||
| `get_current_user` | `GET /api/user/whoami` | `user:profile` |
|
||||
| `get_user_profile` | `GET /api/users/{username}` | public |
|
||||
| `list_my_news` | `GET /api/user/news` | `user:profile` |
|
||||
| `list_my_source_submissions` | `GET /api/user/source-submissions` | `user:profile` |
|
||||
| `list_bookmarks` | `GET /api/user/bookmarks` | `bookmarks:write` |
|
||||
| `update_profile` | `PATCH /api/user/profile` | `user:profile` |
|
||||
| `update_privacy_settings` | `PATCH /api/user/privacy` | `user:profile` |
|
||||
|
||||
### Moderation — `tools/moderation.ts`
|
||||
|
||||
| Tool | Route | Auth |
|
||||
| ---- | ----- | ---- |
|
||||
| `get_mod_queue` | `GET /api/mod/queue` | `mod:queue` Mod/Admin |
|
||||
| `get_mod_stats` | `GET /api/mod/stats` | `mod:stats` Mod/Admin |
|
||||
| `list_mod_users` | `GET /api/mod/users` | `mod:users` Admin |
|
||||
| `get_mod_user` | `GET /api/mod/users/{userId}` | `mod:users` Admin |
|
||||
| `change_user_role` | `PATCH /api/mod/users/{userId}/role` | `mod:users` Admin |
|
||||
| `ban_user` | `POST /api/mod/users/{userId}/ban` | `mod:users` Admin |
|
||||
| `unban_user` | `DELETE /api/mod/users/{userId}/ban` | `mod:users` Admin |
|
||||
| `shadowban_user` | `PATCH /api/mod/users/{userId}/shadowban` | `mod:users` Admin |
|
||||
| `create_mod_user` | `POST /api/mod/users` | `mod:users` Admin |
|
||||
| `list_all_tickets` | `GET /api/mod/tickets` | `mod:queue` Mod/Admin |
|
||||
| `list_mod_comments` | `GET /api/mod/comments` | `mod:queue` Mod/Admin |
|
||||
|
||||
### Tickets — `tools/tickets.ts`
|
||||
|
||||
| Tool | Route | Auth |
|
||||
| ---- | ----- | ---- |
|
||||
| `list_my_tickets` | `GET /api/tickets` | `tickets:read` |
|
||||
| `get_ticket` | `GET /api/tickets/{id}` | `tickets:read` owner or Mod/Admin |
|
||||
| `create_ticket` | `POST /api/tickets` | `tickets:write` |
|
||||
| `reply_to_ticket` | `POST /api/tickets/{id}/reply` | `tickets:write` |
|
||||
| `update_ticket_status` | `PATCH /api/tickets/{id}/status` | `tickets:moderate` Mod/Admin |
|
||||
|
||||
### Legal — `tools/legal.ts`
|
||||
|
||||
| Tool | Route | Auth |
|
||||
| ---- | ----- | ---- |
|
||||
| `get_terms_of_service` | `GET /api/legal/current/terms_of_service` | public |
|
||||
| `get_privacy_policy` | `GET /api/legal/current/privacy_policy` | public |
|
||||
| `list_legal_documents` | `GET /api/legal` | optional; Mod/Admin see drafts |
|
||||
| `accept_legal_documents` | `POST /api/legal/accept` | User+ |
|
||||
|
||||
### Blog — `tools/blog.ts`
|
||||
|
||||
| Tool | Route | Auth |
|
||||
| ---- | ----- | ---- |
|
||||
| `list_blog_posts` | `GET /api/blog` | optional; Mod/Admin see drafts |
|
||||
| `get_blog_post` | `GET /api/blog/{slug}` | optional; Mod/Admin see drafts |
|
||||
| `create_blog_post` | `POST /api/blog` | `blog:write` Mod/Admin |
|
||||
| `update_blog_post` | `PATCH /api/blog/{id}` | `blog:write` Mod/Admin |
|
||||
| `delete_blog_post` | `DELETE /api/blog/{id}` | `blog:write` Mod/Admin |
|
||||
| `publish_blog_post` | `POST /api/blog/{id}/publish` | `blog:write` Mod/Admin |
|
||||
| `unpublish_blog_post` | `POST /api/blog/{id}/unpublish` | `blog:write` Mod/Admin |
|
||||
|
||||
### API Keys — `tools/api-keys.ts`
|
||||
|
||||
| Tool | Route | Auth |
|
||||
| ---- | ----- | ---- |
|
||||
| `list_api_keys` | `GET /api/user/api-keys` | `user:apikeys` |
|
||||
| `create_api_key` | `POST /api/user/api-keys` | cookie session only |
|
||||
| `update_api_key` | `PATCH /api/user/api-keys/{keyId}` | `user:apikeys` |
|
||||
| `revoke_api_key` | `DELETE /api/user/api-keys/{keyId}` | `user:apikeys` |
|
||||
|
||||
### Sessions — `tools/sessions.ts`
|
||||
|
||||
| Tool | Route | Auth |
|
||||
| ---- | ----- | ---- |
|
||||
| `list_sessions` | `GET /api/user/sessions` | cookie session only |
|
||||
| `revoke_all_sessions` | `DELETE /api/user/sessions` | cookie session only |
|
||||
| `revoke_session` | `DELETE /api/user/sessions/{sessionId}` | cookie session only |
|
||||
|
||||
### Discussions — `tools/discussions.ts`
|
||||
|
||||
| Tool | Route | Auth |
|
||||
| ---- | ----- | ---- |
|
||||
| `list_discussions` | `GET /api/discussions` | optional; Mod/Admin see all statuses |
|
||||
| `propose_discussion` | `POST /api/discussions` | User+ (3/hour rate limit) |
|
||||
| `get_discussion` | `GET /api/discussions/{id}` | optional; Mod/Admin see pending/rejected |
|
||||
| `update_discussion` | `PATCH /api/discussions/{id}` | Mod/Admin |
|
||||
| `delete_discussion` | `DELETE /api/discussions/{id}` | Admin |
|
||||
| `list_discussion_messages` | `GET /api/discussions/{id}/messages` | optional; cursor via `before` ISO timestamp |
|
||||
| `delete_discussion_message` | `DELETE /api/discussions/{id}/messages/{messageId}` | owner or Mod/Admin |
|
||||
| `pin_discussion_message` | `POST /api/discussions/{id}/messages/{messageId}/pin` | Mod/Admin |
|
||||
| `list_discussion_bans` | `GET /api/discussions/{id}/bans` | Mod/Admin |
|
||||
| `ban_discussion_user` | `POST /api/discussions/{id}/bans` | Mod/Admin |
|
||||
| `unban_discussion_user` | `DELETE /api/discussions/{id}/bans/{userId}` | Mod/Admin |
|
||||
| `list_mod_discussions` | `GET /api/mod/discussions` | Mod/Admin |
|
||||
| `review_discussion` | `POST /api/mod/discussions/{id}/review` | Mod/Admin |
|
||||
|
||||
---
|
||||
|
||||
## Adding a New Tool
|
||||
|
||||
1. Find or create the right `src/tools/<category>.ts` file.
|
||||
2. Add a `server.tool(name, description, zodSchema, handleTool(async (args) => { ... }))` call inside the register function.
|
||||
3. If creating a new file, export `function registerXxxTools(server, client)` and call it in `src/index.ts`.
|
||||
4. Run `pnpm build` — a clean exit means no type errors.
|
||||
|
||||
Pattern (copy from any existing tool):
|
||||
|
||||
```ts
|
||||
server.tool(
|
||||
"tool_name",
|
||||
"One-line description. Mention required scope.",
|
||||
{
|
||||
id: z.string().uuid().describe("..."),
|
||||
},
|
||||
handleTool(async ({ id }) => {
|
||||
const data = await client.get<unknown>(`/api/route/${id}`);
|
||||
return JSON.stringify(data, null, 2);
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
Query params: pass as second arg to `client.get(url, params)` — undefined values are stripped.
|
||||
Body params: pass as second arg to `client.post/patch(url, body)`.
|
||||
|
||||
---
|
||||
|
||||
## Build & Validate
|
||||
|
||||
```sh
|
||||
pnpm build # tsc — must exit 0 before committing
|
||||
pnpm test # vitest unit tests for client + utils
|
||||
```
|
||||
|
||||
No pre-push script; just `pnpm build` is the gate.
|
||||
@@ -12,6 +12,7 @@ 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";
|
||||
import { registerDiscussionTools } from "./tools/discussions.js";
|
||||
|
||||
const apiKey = process.env.BETTERNEWS_API_KEY;
|
||||
if (!apiKey) {
|
||||
@@ -40,6 +41,7 @@ registerLegalTools(server, client);
|
||||
registerBlogTools(server, client);
|
||||
registerApiKeyTools(server, client);
|
||||
registerSessionTools(server, client);
|
||||
registerDiscussionTools(server, client);
|
||||
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
|
||||
@@ -10,10 +10,11 @@ export function registerCommentTools(server: McpServer, client: BetterNewsClient
|
||||
{
|
||||
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)"),
|
||||
limit: z.number().int().min(1).max(50).optional().describe("Items per page (default 20, max 50)"),
|
||||
sort: z.enum(["top", "most_disliked", "new", "oldest"]).optional().describe("Sort order: top = most liked (default), most_disliked, new = newest first, oldest"),
|
||||
},
|
||||
handleTool(async ({ newsItemId, page, limit }) => {
|
||||
const data = await client.get<unknown>(`/api/news/${newsItemId}/comments`, { page, limit });
|
||||
handleTool(async ({ newsItemId, page, limit, sort }) => {
|
||||
const data = await client.get<unknown>(`/api/news/${newsItemId}/comments`, { page, limit, sort });
|
||||
return JSON.stringify(data, null, 2);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
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 registerDiscussionTools(server: McpServer, client: BetterNewsClient) {
|
||||
server.tool(
|
||||
"list_discussions",
|
||||
"List approved discussion channels. Mod/Admin also see pending/rejected/closed topics. No auth required.",
|
||||
{
|
||||
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)"),
|
||||
},
|
||||
handleTool(async (args) => {
|
||||
const data = await client.get<unknown>("/api/discussions", args);
|
||||
return JSON.stringify(data, null, 2);
|
||||
}),
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"propose_discussion",
|
||||
"Submit a new discussion topic for AI review and moderator approval. Requires authentication. Rate limited to 3 proposals per hour.",
|
||||
{
|
||||
title: z.string().min(5).max(120).describe("Topic title"),
|
||||
description: z.string().min(10).max(1000).describe("What this discussion channel is about"),
|
||||
imageUrl: z.string().url().max(2048).nullable().optional().describe("Optional cover image URL"),
|
||||
},
|
||||
handleTool(async (body) => {
|
||||
const data = await client.post<unknown>("/api/discussions", body);
|
||||
return JSON.stringify(data, null, 2);
|
||||
}),
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"get_discussion",
|
||||
"Get a single discussion topic by UUID. Non-mod users can only see approved/closed topics.",
|
||||
{
|
||||
id: z.string().uuid().describe("Discussion topic UUID"),
|
||||
},
|
||||
handleTool(async ({ id }) => {
|
||||
const data = await client.get<unknown>(`/api/discussions/${id}`);
|
||||
return JSON.stringify(data, null, 2);
|
||||
}),
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"update_discussion",
|
||||
"Update a discussion topic's metadata or status. Requires Mod/Admin role.",
|
||||
{
|
||||
id: z.string().uuid().describe("Discussion topic UUID"),
|
||||
title: z.string().min(5).max(120).optional().describe("New title"),
|
||||
description: z.string().min(10).max(1000).optional().describe("New description"),
|
||||
imageUrl: z.string().url().max(2048).nullable().optional().describe("Cover image URL (null to remove)"),
|
||||
status: z.enum(["approved", "rejected", "closed"]).optional().describe("New status"),
|
||||
slowdownMs: z.number().int().min(0).max(300000).optional().describe("Slowdown in ms between messages (0 = disabled)"),
|
||||
aiReviewNote: z.string().max(500).optional().describe("Override the AI review note"),
|
||||
},
|
||||
handleTool(async ({ id, ...body }) => {
|
||||
const data = await client.patch<unknown>(`/api/discussions/${id}`, body);
|
||||
return JSON.stringify(data, null, 2);
|
||||
}),
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"delete_discussion",
|
||||
"Permanently delete a discussion topic and all its messages. Requires Admin role.",
|
||||
{
|
||||
id: z.string().uuid().describe("Discussion topic UUID"),
|
||||
},
|
||||
handleTool(async ({ id }) => {
|
||||
const data = await client.delete<unknown>(`/api/discussions/${id}`);
|
||||
return JSON.stringify(data, null, 2);
|
||||
}),
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"list_discussion_messages",
|
||||
"Get message history in a discussion channel. Supports cursor-based pagination via 'before'. Mod/Admin also see soft-deleted messages.",
|
||||
{
|
||||
id: z.string().uuid().describe("Discussion topic UUID"),
|
||||
before: z.string().datetime().optional().describe("Cursor: load messages before this ISO timestamp"),
|
||||
limit: z.number().int().min(1).max(100).optional().describe("Max messages to return (default 50, max 100)"),
|
||||
},
|
||||
handleTool(async ({ id, ...params }) => {
|
||||
const data = await client.get<unknown>(`/api/discussions/${id}/messages`, params);
|
||||
return JSON.stringify(data, null, 2);
|
||||
}),
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"delete_discussion_message",
|
||||
"Soft-delete a message in a discussion channel. Owners can delete their own; Mod/Admin can delete any.",
|
||||
{
|
||||
topicId: z.string().uuid().describe("Discussion topic UUID"),
|
||||
messageId: z.string().uuid().describe("Message UUID"),
|
||||
},
|
||||
handleTool(async ({ topicId, messageId }) => {
|
||||
const data = await client.delete<unknown>(`/api/discussions/${topicId}/messages/${messageId}`);
|
||||
return JSON.stringify(data, null, 2);
|
||||
}),
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"pin_discussion_message",
|
||||
"Pin or unpin a message in a discussion channel. Requires Mod/Admin role.",
|
||||
{
|
||||
topicId: z.string().uuid().describe("Discussion topic UUID"),
|
||||
messageId: z.string().uuid().describe("Message UUID"),
|
||||
pinned: z.boolean().describe("True to pin, false to unpin"),
|
||||
},
|
||||
handleTool(async ({ topicId, messageId, pinned }) => {
|
||||
const data = await client.post<unknown>(`/api/discussions/${topicId}/messages/${messageId}/pin`, { pinned });
|
||||
return JSON.stringify(data, null, 2);
|
||||
}),
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"list_discussion_bans",
|
||||
"List all user bans in a discussion channel. Requires Mod/Admin role.",
|
||||
{
|
||||
id: z.string().uuid().describe("Discussion topic UUID"),
|
||||
},
|
||||
handleTool(async ({ id }) => {
|
||||
const data = await client.get<unknown>(`/api/discussions/${id}/bans`);
|
||||
return JSON.stringify(data, null, 2);
|
||||
}),
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"ban_discussion_user",
|
||||
"Ban a user from a discussion channel. Requires Mod/Admin role.",
|
||||
{
|
||||
topicId: z.string().uuid().describe("Discussion topic UUID"),
|
||||
userId: z.string().uuid().describe("UUID of the user to ban"),
|
||||
reason: z.string().min(1).max(500).describe("Reason for the ban"),
|
||||
expiresAt: z.string().datetime().nullish().describe("Optional ban expiry (ISO 8601). Omit for permanent."),
|
||||
},
|
||||
handleTool(async ({ topicId, ...body }) => {
|
||||
const data = await client.post<unknown>(`/api/discussions/${topicId}/bans`, body);
|
||||
return JSON.stringify(data, null, 2);
|
||||
}),
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"unban_discussion_user",
|
||||
"Remove a user's ban from a discussion channel. Requires Mod/Admin role.",
|
||||
{
|
||||
topicId: z.string().uuid().describe("Discussion topic UUID"),
|
||||
userId: z.string().uuid().describe("UUID of the user to unban"),
|
||||
},
|
||||
handleTool(async ({ topicId, userId }) => {
|
||||
const data = await client.delete<unknown>(`/api/discussions/${topicId}/bans/${userId}`);
|
||||
return JSON.stringify(data, null, 2);
|
||||
}),
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"list_mod_discussions",
|
||||
"List discussion topic submissions with moderation detail (AI review result, message/ban counts). Requires Mod/Admin role.",
|
||||
{
|
||||
status: z.enum(["pending", "approved", "rejected", "closed"]).optional().describe("Filter by status"),
|
||||
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)"),
|
||||
},
|
||||
handleTool(async (args) => {
|
||||
const data = await client.get<unknown>("/api/mod/discussions", args);
|
||||
return JSON.stringify(data, null, 2);
|
||||
}),
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"review_discussion",
|
||||
"Approve or reject a pending discussion topic submission. Requires Mod/Admin role.",
|
||||
{
|
||||
id: z.string().uuid().describe("Discussion topic UUID"),
|
||||
action: z.enum(["approve", "reject", "reject_with_ai"]).describe("'approve', 'reject' with an optional note, or 'reject_with_ai' to use the stored AI review note as the rejection reason"),
|
||||
note: z.string().max(500).optional().describe("Optional moderator note (used as rejection reason when action is 'reject')"),
|
||||
},
|
||||
handleTool(async ({ id, ...body }) => {
|
||||
const data = await client.post<unknown>(`/api/mod/discussions/${id}/review`, body);
|
||||
return JSON.stringify(data, null, 2);
|
||||
}),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user