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:
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.env
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
# betternews-mcp
|
||||||
|
|
||||||
|
MCP server for the [BetterNews](https://betternews.app) API. Works with Claude, Codex, OpenClaw, and any other MCP-compatible client.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### 1. Get an API key
|
||||||
|
|
||||||
|
Create one at **Settings → API Keys** on [betternews.app](https://betternews.app).
|
||||||
|
|
||||||
|
Choose only the scopes you need. Available scopes:
|
||||||
|
|
||||||
|
| Scope | Access |
|
||||||
|
|---|---|
|
||||||
|
| `news:read` | List and search news |
|
||||||
|
| `news:write` | Create, edit, react, bookmark |
|
||||||
|
| `news:comment` | Post and delete comments |
|
||||||
|
| `news:moderate` | Review items and source submissions |
|
||||||
|
| `sources:read` | List sources |
|
||||||
|
| `sources:write` | Create/edit sources (Mod/Admin) |
|
||||||
|
| `bookmarks:write` | Manage bookmarks |
|
||||||
|
| `tickets:read` | View own tickets |
|
||||||
|
| `tickets:write` | Open tickets and reply |
|
||||||
|
| `tickets:moderate` | Manage all tickets (Mod/Admin) |
|
||||||
|
| `mod:queue` | Access mod queue and tickets |
|
||||||
|
| `mod:stats` | View moderation statistics |
|
||||||
|
| `mod:users` | Manage users (Admin) |
|
||||||
|
| `user:profile` | Read own profile and news |
|
||||||
|
| `user:apikeys` | List own API keys |
|
||||||
|
| `privacy:read` | Export personal data |
|
||||||
|
|
||||||
|
### 2. Install and build
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm install
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Configure your MCP client
|
||||||
|
|
||||||
|
#### Claude Desktop / Claude Code
|
||||||
|
|
||||||
|
Add to your `claude_desktop_config.json` (or project `.mcp.json`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"betternews": {
|
||||||
|
"command": "node",
|
||||||
|
"args": ["/absolute/path/to/mcp-server/dist/index.js"],
|
||||||
|
"env": {
|
||||||
|
"BETTERNEWS_API_KEY": "bn_sk_your_key_here"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Codex CLI
|
||||||
|
|
||||||
|
```sh
|
||||||
|
BETTERNEWS_API_KEY=bn_sk_your_key_here codex --mcp "node /path/to/mcp-server/dist/index.js"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### OpenClaw
|
||||||
|
|
||||||
|
Add as a stdio MCP tool with `command: node dist/index.js` and the `BETTERNEWS_API_KEY` env var.
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
|
||||||
|
### News
|
||||||
|
|
||||||
|
| Tool | Description | Required scope |
|
||||||
|
|---|---|---|
|
||||||
|
| `get_top_news` | Top 5 news by weighted score | — |
|
||||||
|
| `list_news` | Paginated news list with filters | `news:read` |
|
||||||
|
| `get_news_item` | Single news item by ID | `news:read` |
|
||||||
|
| `search_news` | Semantic vector search | `news:read` |
|
||||||
|
| `get_similar_news` | Vector-similar articles | `news:read` |
|
||||||
|
| `create_news_item` | Create a draft | `news:write` |
|
||||||
|
| `update_news_item` | Edit an item | `news:write` |
|
||||||
|
| `submit_news_item` | Submit draft for review | `news:write` |
|
||||||
|
| `review_news_item` | Approve / reject / revise | `news:moderate` |
|
||||||
|
| `like_news_item` | Toggle like | `news:write` |
|
||||||
|
| `dislike_news_item` | Toggle dislike | `news:write` |
|
||||||
|
| `bookmark_news_item` | Toggle bookmark | `bookmarks:write` |
|
||||||
|
| `delete_news_item` | Delete an item | `news:write` |
|
||||||
|
| `get_news_item_stats` | Engagement stats | Mod/Admin |
|
||||||
|
|
||||||
|
### Comments
|
||||||
|
|
||||||
|
| Tool | Description | Required scope |
|
||||||
|
|---|---|---|
|
||||||
|
| `list_comments` | Comments on a news item | `news:read` |
|
||||||
|
| `create_comment` | Post a comment | `news:comment` |
|
||||||
|
| `delete_comment` | Delete a comment | `news:comment` |
|
||||||
|
|
||||||
|
### Sources
|
||||||
|
|
||||||
|
| Tool | Description | Required scope |
|
||||||
|
|---|---|---|
|
||||||
|
| `list_sources` | Paginated source list | `sources:read` |
|
||||||
|
| `get_source` | Single source by ID | `sources:read` |
|
||||||
|
| `create_source` | Create a source | `sources:write` |
|
||||||
|
| `update_source` | Edit a source | `sources:write` |
|
||||||
|
| `list_source_submissions` | Source suggestions on an item | `news:read` |
|
||||||
|
| `submit_source` | Suggest a source | `news:write` |
|
||||||
|
| `review_source_submission` | Approve / reject suggestion | `news:moderate` |
|
||||||
|
|
||||||
|
### User
|
||||||
|
|
||||||
|
| Tool | Description | Required scope |
|
||||||
|
|---|---|---|
|
||||||
|
| `get_current_user` | Own profile and account details | `user:profile` |
|
||||||
|
| `get_user_profile` | Public profile by username | — |
|
||||||
|
| `list_my_news` | Own news items | `user:profile` |
|
||||||
|
| `list_my_source_submissions` | Own source submissions | `user:profile` |
|
||||||
|
| `list_bookmarks` | Bookmarked news items | `bookmarks:write` |
|
||||||
|
| `update_profile` | Update display name / email / bio | `user:profile` |
|
||||||
|
|
||||||
|
### Moderation
|
||||||
|
|
||||||
|
| Tool | Description | Required scope |
|
||||||
|
|---|---|---|
|
||||||
|
| `get_mod_queue` | Pending review queue | `mod:queue` |
|
||||||
|
| `get_mod_stats` | Aggregate statistics | `mod:stats` |
|
||||||
|
| `list_mod_users` | All users (Admin) | `mod:users` |
|
||||||
|
| `get_mod_user` | User detail (Admin) | `mod:users` |
|
||||||
|
| `change_user_role` | Change role (Admin) | `mod:users` |
|
||||||
|
| `ban_user` | Ban a user (Admin) | `mod:users` |
|
||||||
|
| `unban_user` | Lift a ban (Admin) | `mod:users` |
|
||||||
|
| `list_all_tickets` | All tickets (Mod view) | `mod:queue` |
|
||||||
|
|
||||||
|
### Tickets
|
||||||
|
|
||||||
|
| Tool | Description | Required scope |
|
||||||
|
|---|---|---|
|
||||||
|
| `list_my_tickets` | Own support tickets | `tickets:read` |
|
||||||
|
| `get_ticket` | Ticket with reply history | `tickets:read` |
|
||||||
|
| `create_ticket` | Open a ticket | `tickets:write` |
|
||||||
|
| `reply_to_ticket` | Reply to a ticket | `tickets:write` |
|
||||||
|
| `update_ticket_status` | Change ticket status (Mod) | `tickets:moderate` |
|
||||||
|
|
||||||
|
### Legal
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|---|---|
|
||||||
|
| `get_terms_of_service` | Current published ToS |
|
||||||
|
| `get_privacy_policy` | Current published Privacy Policy |
|
||||||
|
| `list_legal_documents` | List all docs (Mod/Admin see drafts) |
|
||||||
|
| `accept_legal_documents` | Accept current legal docs |
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
| Variable | Required | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `BETTERNEWS_API_KEY` | Yes | API key with prefix `bn_sk_` |
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "betternews-mcp",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "MCP server for the BetterNews API",
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"bin": {
|
||||||
|
"betternews-mcp": "dist/index.js"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"dev": "tsc --watch",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"prepublishOnly": "pnpm build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||||
|
"zod": "^4.4.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.19.21",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
const BASE_URL = "https://api.betternews.app";
|
||||||
|
|
||||||
|
export class BetterNewsClient {
|
||||||
|
private apiKey: string;
|
||||||
|
|
||||||
|
constructor(apiKey: string) {
|
||||||
|
this.apiKey = apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
async request<T>(
|
||||||
|
method: string,
|
||||||
|
path: string,
|
||||||
|
body?: unknown,
|
||||||
|
query?: Record<string, string | number | boolean | undefined>,
|
||||||
|
): Promise<T> {
|
||||||
|
let url = `${BASE_URL}${path}`;
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
for (const [key, value] of Object.entries(query)) {
|
||||||
|
if (value !== undefined) {
|
||||||
|
params.set(key, String(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const qs = params.toString();
|
||||||
|
if (qs) url += `?${qs}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.apiKey}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
let detail = "";
|
||||||
|
try {
|
||||||
|
const err = (await res.json()) as { message?: string };
|
||||||
|
detail = err.message ?? "";
|
||||||
|
} catch {
|
||||||
|
detail = await res.text().catch(() => "");
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`BetterNews API error ${res.status}: ${detail || res.statusText}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json() as Promise<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
get<T>(
|
||||||
|
path: string,
|
||||||
|
query?: Record<string, string | number | boolean | undefined>,
|
||||||
|
) {
|
||||||
|
return this.request<T>("GET", path, undefined, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
post<T>(path: string, body?: unknown) {
|
||||||
|
return this.request<T>("POST", path, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
patch<T>(path: string, body?: unknown) {
|
||||||
|
return this.request<T>("PATCH", path, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete<T>(path: string) {
|
||||||
|
return this.request<T>("DELETE", path);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||||
|
import { BetterNewsClient } from "./client.js";
|
||||||
|
import { registerNewsTools } from "./tools/news.js";
|
||||||
|
import { registerCommentTools } from "./tools/comments.js";
|
||||||
|
import { registerSourceTools } from "./tools/sources.js";
|
||||||
|
import { registerUserTools } from "./tools/user.js";
|
||||||
|
import { registerModerationTools } from "./tools/moderation.js";
|
||||||
|
import { registerTicketTools } from "./tools/tickets.js";
|
||||||
|
import { registerLegalTools } from "./tools/legal.js";
|
||||||
|
|
||||||
|
const apiKey = process.env.BETTERNEWS_API_KEY;
|
||||||
|
if (!apiKey) {
|
||||||
|
process.stderr.write(
|
||||||
|
"Error: BETTERNEWS_API_KEY environment variable is required.\n" +
|
||||||
|
"Create an API key at https://betternews.app/settings/api-keys and set it.\n",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new BetterNewsClient(apiKey);
|
||||||
|
|
||||||
|
const server = new McpServer({
|
||||||
|
name: "betternews",
|
||||||
|
version: "1.0.0",
|
||||||
|
});
|
||||||
|
|
||||||
|
registerNewsTools(server, client);
|
||||||
|
registerCommentTools(server, client);
|
||||||
|
registerSourceTools(server, client);
|
||||||
|
registerUserTools(server, client);
|
||||||
|
registerModerationTools(server, client);
|
||||||
|
registerTicketTools(server, client);
|
||||||
|
registerLegalTools(server, client);
|
||||||
|
|
||||||
|
const transport = new StdioServerTransport();
|
||||||
|
await server.connect(transport);
|
||||||
@@ -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) }] };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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) }] };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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) }] };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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) }] };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 0–100"),
|
||||||
|
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) }] };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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) }] };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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) }] };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "Node16",
|
||||||
|
"moduleResolution": "Node16",
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"declaration": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user