bc42508849
CI / Build & Test (push) Successful in 17s
- New polls.ts: 13 tools covering full poll lifecycle (create/list/get/ update/delete/vote/like/dislike/review) and poll comments - news.ts: get_deep_dive and regenerate_deep_dive for AI analysis - tickets.ts: create_ticket_message with internal-note and attachment support - moderation.ts: list/update/revoke mod-managed user API keys - user.ts: list_my_polls and list_my_poll_votes - Bump server version to 1.2.0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
232 lines
11 KiB
Markdown
232 lines
11 KiB
Markdown
# 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` | 24 news tools |
|
|
| `src/tools/comments.ts` | 6 comment tools |
|
|
| `src/tools/sources.ts` | 7 source tools |
|
|
| `src/tools/user.ts` | 9 user/profile tools |
|
|
| `src/tools/moderation.ts` | 14 moderation tools |
|
|
| `src/tools/tickets.ts` | 6 support ticket 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 |
|
|
| `src/tools/polls.ts` | 13 poll 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` |
|
|
| `get_deep_dive` | `GET /api/news/{id}/deep-dive` | optional |
|
|
| `regenerate_deep_dive` | `POST /api/news/{id}/deep-dive/regenerate` | `news:moderate` Mod/Admin |
|
|
|
|
### 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` |
|
|
| `list_my_polls` | `GET /api/user/polls` | `user:profile` |
|
|
| `list_my_poll_votes` | `GET /api/user/poll-votes` | `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 |
|
|
| `list_mod_user_api_keys` | `GET /api/mod/users/{userId}/api-keys` | `mod:users` Admin |
|
|
| `update_mod_user_api_key` | `PATCH /api/mod/users/{userId}/api-keys/{keyId}` | `mod:users` Admin |
|
|
| `revoke_mod_user_api_key` | `DELETE /api/mod/users/{userId}/api-keys/{keyId}` | `mod:users` 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` |
|
|
| `create_ticket_message` | `POST /api/tickets/{id}/messages` | `tickets:write` / `tickets:moderate` |
|
|
| `update_ticket_status` | `PATCH /api/tickets/{id}/status` | `tickets:moderate` Mod/Admin |
|
|
|
|
### 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 |
|
|
|
|
### Polls — `tools/polls.ts`
|
|
|
|
| Tool | Route | Auth |
|
|
| ---- | ----- | ---- |
|
|
| `list_polls` | `GET /api/polls` | optional; non-published statuses require Mod/Admin |
|
|
| `create_poll` | `POST /api/polls` | `polls:write` + email verified |
|
|
| `get_poll` | `GET /api/polls/{id}` | optional; non-published only for author or Mod/Admin |
|
|
| `update_poll` | `PATCH /api/polls/{id}` | `polls:write` (owner) or `polls:moderate` (Mod/Admin) |
|
|
| `delete_poll` | `DELETE /api/polls/{id}` | `polls:write` (owner pending_review only) or Mod/Admin |
|
|
| `vote_poll` | `POST /api/polls/{id}/vote` | `polls:write` |
|
|
| `like_poll` | `POST /api/polls/{id}/like` | `polls:write` |
|
|
| `dislike_poll` | `POST /api/polls/{id}/dislike` | `polls:write` |
|
|
| `review_poll` | `POST /api/polls/{id}/review` | `polls:moderate` Mod/Admin |
|
|
| `list_poll_comments` | `GET /api/polls/{id}/comments` | optional |
|
|
| `create_poll_comment` | `POST /api/polls/{id}/comments` | `polls:comment` + email verified |
|
|
| `hide_poll_comment` | `PATCH /api/polls/{id}/comments/{commentId}` | `polls:moderate` Mod/Admin |
|
|
| `delete_poll_comment` | `DELETE /api/polls/{id}/comments/{commentId}` | owner or `polls:comment` 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.
|