fix: apply all A-J improvements across MCP server
- A: fix get_user_profile URL (/api/user/profile → /api/users/:username)
- B: fix unban_user to use DELETE /api/mod/users/:id/ban (was POST .../unban)
- C: add check_duplicates tool (GET /api/news/:id/check-duplicates)
- D: add shadowban_user tool (PATCH /api/mod/users/:id/shadowban)
- E: add create_mod_user tool (POST /api/mod/users)
- F: extract handleTool helper in utils.ts; all 37 tools now use it for
consistent isError:true error responses visible to the AI model
- G: only send Content-Type header when request has a body
- H: support BETTERNEWS_BASE_URL env var for local dev overrides
- I: fix get_news_item_stats description (accessible by own author, not Mod/Admin only)
- J: add .describe() to page/limit/status params in list_all_tickets and list_my_tickets
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+13
-9
@@ -1,10 +1,10 @@
|
|||||||
const BASE_URL = "https://api.betternews.app";
|
|
||||||
|
|
||||||
export class BetterNewsClient {
|
export class BetterNewsClient {
|
||||||
private apiKey: string;
|
private apiKey: string;
|
||||||
|
private baseUrl: string;
|
||||||
|
|
||||||
constructor(apiKey: string) {
|
constructor(apiKey: string, baseUrl = "https://api.betternews.app") {
|
||||||
this.apiKey = apiKey;
|
this.apiKey = apiKey;
|
||||||
|
this.baseUrl = baseUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
async request<T>(
|
async request<T>(
|
||||||
@@ -13,7 +13,7 @@ export class BetterNewsClient {
|
|||||||
body?: unknown,
|
body?: unknown,
|
||||||
query?: Record<string, string | number | boolean | undefined>,
|
query?: Record<string, string | number | boolean | undefined>,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
let url = `${BASE_URL}${path}`;
|
let url = `${this.baseUrl}${path}`;
|
||||||
|
|
||||||
if (query) {
|
if (query) {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
@@ -26,13 +26,17 @@ export class BetterNewsClient {
|
|||||||
if (qs) url += `?${qs}`;
|
if (qs) url += `?${qs}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
Authorization: `Bearer ${this.apiKey}`,
|
||||||
|
Accept: "application/json",
|
||||||
|
};
|
||||||
|
if (body !== undefined) {
|
||||||
|
headers["Content-Type"] = "application/json";
|
||||||
|
}
|
||||||
|
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
method,
|
method,
|
||||||
headers: {
|
headers,
|
||||||
Authorization: `Bearer ${this.apiKey}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Accept: "application/json",
|
|
||||||
},
|
|
||||||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -19,7 +19,8 @@ if (!apiKey) {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = new BetterNewsClient(apiKey);
|
const baseUrl = process.env.BETTERNEWS_BASE_URL ?? "https://api.betternews.app";
|
||||||
|
const client = new BetterNewsClient(apiKey, baseUrl);
|
||||||
|
|
||||||
const server = new McpServer({
|
const server = new McpServer({
|
||||||
name: "betternews",
|
name: "betternews",
|
||||||
|
|||||||
+10
-9
@@ -1,6 +1,7 @@
|
|||||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import type { BetterNewsClient } from "../client.js";
|
import type { BetterNewsClient } from "../client.js";
|
||||||
|
import { handleTool } from "../utils.js";
|
||||||
|
|
||||||
export function registerCommentTools(server: McpServer, client: BetterNewsClient) {
|
export function registerCommentTools(server: McpServer, client: BetterNewsClient) {
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -11,10 +12,10 @@ export function registerCommentTools(server: McpServer, client: BetterNewsClient
|
|||||||
page: z.number().int().positive().optional().describe("Page number (default 1)"),
|
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(100).optional().describe("Items per page (default 20)"),
|
||||||
},
|
},
|
||||||
async ({ newsItemId, page, limit }) => {
|
handleTool(async ({ newsItemId, page, limit }) => {
|
||||||
const data = await client.get<unknown>(`/api/news/${newsItemId}/comments`, { page, limit });
|
const data = await client.get<unknown>(`/api/news/${newsItemId}/comments`, { page, limit });
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -24,10 +25,10 @@ export function registerCommentTools(server: McpServer, client: BetterNewsClient
|
|||||||
newsItemId: z.string().uuid().describe("News item UUID"),
|
newsItemId: z.string().uuid().describe("News item UUID"),
|
||||||
content: z.string().min(1).max(2000).describe("Comment text"),
|
content: z.string().min(1).max(2000).describe("Comment text"),
|
||||||
},
|
},
|
||||||
async ({ newsItemId, content }) => {
|
handleTool(async ({ newsItemId, content }) => {
|
||||||
const data = await client.post<unknown>(`/api/news/${newsItemId}/comments`, { content });
|
const data = await client.post<unknown>(`/api/news/${newsItemId}/comments`, { content });
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -37,9 +38,9 @@ export function registerCommentTools(server: McpServer, client: BetterNewsClient
|
|||||||
newsItemId: z.string().uuid().describe("News item UUID"),
|
newsItemId: z.string().uuid().describe("News item UUID"),
|
||||||
commentId: z.string().uuid().describe("Comment UUID"),
|
commentId: z.string().uuid().describe("Comment UUID"),
|
||||||
},
|
},
|
||||||
async ({ newsItemId, commentId }) => {
|
handleTool(async ({ newsItemId, commentId }) => {
|
||||||
const data = await client.delete<unknown>(`/api/news/${newsItemId}/comments/${commentId}`);
|
const data = await client.delete<unknown>(`/api/news/${newsItemId}/comments/${commentId}`);
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-14
@@ -1,26 +1,27 @@
|
|||||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import type { BetterNewsClient } from "../client.js";
|
import type { BetterNewsClient } from "../client.js";
|
||||||
|
import { handleTool } from "../utils.js";
|
||||||
|
|
||||||
export function registerLegalTools(server: McpServer, client: BetterNewsClient) {
|
export function registerLegalTools(server: McpServer, client: BetterNewsClient) {
|
||||||
server.tool(
|
server.tool(
|
||||||
"get_terms_of_service",
|
"get_terms_of_service",
|
||||||
"Get the current published Terms of Service document.",
|
"Get the current published Terms of Service document.",
|
||||||
{},
|
{},
|
||||||
async () => {
|
handleTool(async () => {
|
||||||
const data = await client.get<unknown>("/api/legal/current/terms_of_service");
|
const data = await client.get<unknown>("/api/legal/current/terms_of_service");
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
"get_privacy_policy",
|
"get_privacy_policy",
|
||||||
"Get the current published Privacy Policy document.",
|
"Get the current published Privacy Policy document.",
|
||||||
{},
|
{},
|
||||||
async () => {
|
handleTool(async () => {
|
||||||
const data = await client.get<unknown>("/api/legal/current/privacy_policy");
|
const data = await client.get<unknown>("/api/legal/current/privacy_policy");
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -29,22 +30,22 @@ export function registerLegalTools(server: McpServer, client: BetterNewsClient)
|
|||||||
{
|
{
|
||||||
type: z.enum(["terms_of_service", "privacy_policy"]).optional().describe("Filter by document type"),
|
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)"),
|
status: z.enum(["draft", "published", "archived"]).optional().describe("Filter by status (Mod/Admin only)"),
|
||||||
page: z.number().int().positive().optional(),
|
page: z.number().int().positive().optional().describe("Page number (default 1)"),
|
||||||
limit: z.number().int().min(1).max(50).optional(),
|
limit: z.number().int().min(1).max(50).optional().describe("Items per page (default 20)"),
|
||||||
},
|
},
|
||||||
async (args) => {
|
handleTool(async (args) => {
|
||||||
const data = await client.get<unknown>("/api/legal", args);
|
const data = await client.get<unknown>("/api/legal", args);
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
"accept_legal_documents",
|
"accept_legal_documents",
|
||||||
"Accept the current legal documents (Terms of Service and Privacy Policy). Clears the legalAcceptanceRequired flag.",
|
"Accept the current legal documents (Terms of Service and Privacy Policy). Clears the legalAcceptanceRequired flag.",
|
||||||
{},
|
{},
|
||||||
async () => {
|
handleTool(async () => {
|
||||||
const data = await client.post<unknown>("/api/legal/accept");
|
const data = await client.post<unknown>("/api/legal/accept");
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+57
-27
@@ -1,6 +1,7 @@
|
|||||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import type { BetterNewsClient } from "../client.js";
|
import type { BetterNewsClient } from "../client.js";
|
||||||
|
import { handleTool } from "../utils.js";
|
||||||
|
|
||||||
export function registerModerationTools(server: McpServer, client: BetterNewsClient) {
|
export function registerModerationTools(server: McpServer, client: BetterNewsClient) {
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -10,20 +11,20 @@ export function registerModerationTools(server: McpServer, client: BetterNewsCli
|
|||||||
page: z.number().int().positive().optional().describe("Page number (default 1)"),
|
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)"),
|
limit: z.number().int().min(1).max(50).optional().describe("Items per page (default 20)"),
|
||||||
},
|
},
|
||||||
async (args) => {
|
handleTool(async (args) => {
|
||||||
const data = await client.get<unknown>("/api/mod/queue", args);
|
const data = await client.get<unknown>("/api/mod/queue", args);
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
"get_mod_stats",
|
"get_mod_stats",
|
||||||
"Get aggregate moderation statistics. Requires Mod/Admin role and mod:stats scope.",
|
"Get aggregate moderation statistics. Requires Mod/Admin role and mod:stats scope.",
|
||||||
{},
|
{},
|
||||||
async () => {
|
handleTool(async () => {
|
||||||
const data = await client.get<unknown>("/api/mod/stats");
|
const data = await client.get<unknown>("/api/mod/stats");
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -36,10 +37,10 @@ export function registerModerationTools(server: McpServer, client: BetterNewsCli
|
|||||||
role: z.enum(["User", "Moderator", "Admin"]).optional().describe("Filter by role"),
|
role: z.enum(["User", "Moderator", "Admin"]).optional().describe("Filter by role"),
|
||||||
banned: z.boolean().optional().describe("Filter by ban status"),
|
banned: z.boolean().optional().describe("Filter by ban status"),
|
||||||
},
|
},
|
||||||
async (args) => {
|
handleTool(async (args) => {
|
||||||
const data = await client.get<unknown>("/api/mod/users", args);
|
const data = await client.get<unknown>("/api/mod/users", args);
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -48,10 +49,10 @@ export function registerModerationTools(server: McpServer, client: BetterNewsCli
|
|||||||
{
|
{
|
||||||
userId: z.string().uuid().describe("User UUID"),
|
userId: z.string().uuid().describe("User UUID"),
|
||||||
},
|
},
|
||||||
async ({ userId }) => {
|
handleTool(async ({ userId }) => {
|
||||||
const data = await client.get<unknown>(`/api/mod/users/${userId}`);
|
const data = await client.get<unknown>(`/api/mod/users/${userId}`);
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -61,10 +62,10 @@ export function registerModerationTools(server: McpServer, client: BetterNewsCli
|
|||||||
userId: z.string().uuid().describe("User UUID"),
|
userId: z.string().uuid().describe("User UUID"),
|
||||||
role: z.enum(["User", "Moderator", "Admin"]).describe("New role"),
|
role: z.enum(["User", "Moderator", "Admin"]).describe("New role"),
|
||||||
},
|
},
|
||||||
async ({ userId, role }) => {
|
handleTool(async ({ userId, role }) => {
|
||||||
const data = await client.patch<unknown>(`/api/mod/users/${userId}/role`, { role });
|
const data = await client.patch<unknown>(`/api/mod/users/${userId}/role`, { role });
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -76,10 +77,10 @@ export function registerModerationTools(server: McpServer, client: BetterNewsCli
|
|||||||
bannedUntil: z.string().datetime().optional().describe("Temporary ban expiry (ISO 8601). Omit for permanent."),
|
bannedUntil: z.string().datetime().optional().describe("Temporary ban expiry (ISO 8601). Omit for permanent."),
|
||||||
permanent: z.boolean().optional().describe("Whether the ban is permanent"),
|
permanent: z.boolean().optional().describe("Whether the ban is permanent"),
|
||||||
},
|
},
|
||||||
async ({ userId, ...body }) => {
|
handleTool(async ({ userId, ...body }) => {
|
||||||
const data = await client.post<unknown>(`/api/mod/users/${userId}/ban`, body);
|
const data = await client.post<unknown>(`/api/mod/users/${userId}/ban`, body);
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -88,23 +89,52 @@ export function registerModerationTools(server: McpServer, client: BetterNewsCli
|
|||||||
{
|
{
|
||||||
userId: z.string().uuid().describe("User UUID"),
|
userId: z.string().uuid().describe("User UUID"),
|
||||||
},
|
},
|
||||||
async ({ userId }) => {
|
handleTool(async ({ userId }) => {
|
||||||
const data = await client.post<unknown>(`/api/mod/users/${userId}/unban`);
|
const data = await client.delete<unknown>(`/api/mod/users/${userId}/ban`);
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
server.tool(
|
||||||
|
"shadowban_user",
|
||||||
|
"Toggle shadowban on a user. Shadowbanned users can post but their content is invisible to others. Requires Admin role and mod:users scope.",
|
||||||
|
{
|
||||||
|
userId: z.string().uuid().describe("User UUID"),
|
||||||
|
shadowbanned: z.boolean().describe("True to shadowban, false to remove shadowban"),
|
||||||
},
|
},
|
||||||
|
handleTool(async ({ userId, shadowbanned }) => {
|
||||||
|
const data = await client.patch<unknown>(`/api/mod/users/${userId}/shadowban`, { shadowbanned });
|
||||||
|
return JSON.stringify(data, null, 2);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
server.tool(
|
||||||
|
"create_mod_user",
|
||||||
|
"Create a new user account directly (bypasses email verification). Requires Admin role and mod:users scope.",
|
||||||
|
{
|
||||||
|
email: z.string().email().describe("User email address"),
|
||||||
|
username: z.string().min(3).max(30).describe("Username"),
|
||||||
|
displayName: z.string().min(1).max(25).describe("Display name"),
|
||||||
|
password: z.string().min(8).describe("Initial password"),
|
||||||
|
role: z.enum(["User", "Moderator", "Admin"]).optional().describe("Role (default: User)"),
|
||||||
|
},
|
||||||
|
handleTool(async (body) => {
|
||||||
|
const data = await client.post<unknown>("/api/mod/users", body);
|
||||||
|
return JSON.stringify(data, null, 2);
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
"list_all_tickets",
|
"list_all_tickets",
|
||||||
"List all support tickets (mod view). Requires Mod/Admin role and mod:queue scope.",
|
"List all support tickets (mod view). Requires Mod/Admin role and mod:queue scope.",
|
||||||
{
|
{
|
||||||
page: z.number().int().positive().optional(),
|
page: z.number().int().positive().optional().describe("Page number (default 1)"),
|
||||||
limit: z.number().int().min(1).max(100).optional(),
|
limit: z.number().int().min(1).max(100).optional().describe("Items per page (default 20)"),
|
||||||
status: z.enum(["open", "in_progress", "resolved", "closed"]).optional(),
|
status: z.enum(["open", "in_progress", "resolved", "closed"]).optional().describe("Filter by status"),
|
||||||
},
|
},
|
||||||
async (args) => {
|
handleTool(async (args) => {
|
||||||
const data = await client.get<unknown>("/api/mod/tickets", args);
|
const data = await client.get<unknown>("/api/mod/tickets", args);
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+69
-56
@@ -1,16 +1,17 @@
|
|||||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import type { BetterNewsClient } from "../client.js";
|
import type { BetterNewsClient } from "../client.js";
|
||||||
|
import { handleTool } from "../utils.js";
|
||||||
|
|
||||||
export function registerNewsTools(server: McpServer, client: BetterNewsClient) {
|
export function registerNewsTools(server: McpServer, client: BetterNewsClient) {
|
||||||
server.tool(
|
server.tool(
|
||||||
"get_top_news",
|
"get_top_news",
|
||||||
"Get the top 5 news items by weighted score (likes + recency). No auth required.",
|
"Get the top 5 news items by weighted score (likes + recency). No auth required.",
|
||||||
{},
|
{},
|
||||||
async () => {
|
handleTool(async () => {
|
||||||
const data = await client.get<unknown>("/api/news/top");
|
const data = await client.get<unknown>("/api/news/top");
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -28,10 +29,10 @@ export function registerNewsTools(server: McpServer, client: BetterNewsClient) {
|
|||||||
.describe("Filter by status (Mod/Admin only for non-published)"),
|
.describe("Filter by status (Mod/Admin only for non-published)"),
|
||||||
search: z.string().optional().describe("Text search over title and summary"),
|
search: z.string().optional().describe("Text search over title and summary"),
|
||||||
},
|
},
|
||||||
async (args) => {
|
handleTool(async (args) => {
|
||||||
const data = await client.get<unknown>("/api/news", args);
|
const data = await client.get<unknown>("/api/news", args);
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -40,10 +41,10 @@ export function registerNewsTools(server: McpServer, client: BetterNewsClient) {
|
|||||||
{
|
{
|
||||||
id: z.string().uuid().describe("News item UUID"),
|
id: z.string().uuid().describe("News item UUID"),
|
||||||
},
|
},
|
||||||
async ({ id }) => {
|
handleTool(async ({ id }) => {
|
||||||
const data = await client.get<unknown>(`/api/news/${id}`);
|
const data = await client.get<unknown>(`/api/news/${id}`);
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -53,10 +54,10 @@ export function registerNewsTools(server: McpServer, client: BetterNewsClient) {
|
|||||||
q: z.string().describe("Search query"),
|
q: z.string().describe("Search query"),
|
||||||
limit: z.number().int().min(1).max(20).optional().describe("Max results (default 10)"),
|
limit: z.number().int().min(1).max(20).optional().describe("Max results (default 10)"),
|
||||||
},
|
},
|
||||||
async ({ q, limit }) => {
|
handleTool(async ({ q, limit }) => {
|
||||||
const data = await client.get<unknown>("/api/news/search", { q, limit });
|
const data = await client.get<unknown>("/api/news/search", { q, limit });
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -66,10 +67,22 @@ export function registerNewsTools(server: McpServer, client: BetterNewsClient) {
|
|||||||
id: z.string().uuid().describe("News item UUID"),
|
id: z.string().uuid().describe("News item UUID"),
|
||||||
limit: z.number().int().min(1).max(10).optional().describe("Max results (default 5)"),
|
limit: z.number().int().min(1).max(10).optional().describe("Max results (default 5)"),
|
||||||
},
|
},
|
||||||
async ({ id, limit }) => {
|
handleTool(async ({ id, limit }) => {
|
||||||
const data = await client.get<unknown>(`/api/news/${id}/similar`, { limit });
|
const data = await client.get<unknown>(`/api/news/${id}/similar`, { limit });
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
server.tool(
|
||||||
|
"check_duplicates",
|
||||||
|
"Check a draft news item for near-duplicate published or pending articles using vector similarity. Call this before submitting a draft for review. Requires news:moderate scope.",
|
||||||
|
{
|
||||||
|
id: z.string().uuid().describe("News item UUID to check"),
|
||||||
},
|
},
|
||||||
|
handleTool(async ({ id }) => {
|
||||||
|
const data = await client.get<unknown>(`/api/news/${id}/check-duplicates`);
|
||||||
|
return JSON.stringify(data, null, 2);
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -91,10 +104,10 @@ export function registerNewsTools(server: McpServer, client: BetterNewsClient) {
|
|||||||
imageCredits: z.string().optional().describe("Image attribution"),
|
imageCredits: z.string().optional().describe("Image attribution"),
|
||||||
datePublished: z.string().datetime().optional().describe("Original publication date (ISO 8601)"),
|
datePublished: z.string().datetime().optional().describe("Original publication date (ISO 8601)"),
|
||||||
},
|
},
|
||||||
async (args) => {
|
handleTool(async (args) => {
|
||||||
const data = await client.post<unknown>("/api/news", args);
|
const data = await client.post<unknown>("/api/news", args);
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -103,10 +116,10 @@ export function registerNewsTools(server: McpServer, client: BetterNewsClient) {
|
|||||||
{
|
{
|
||||||
id: z.string().uuid().describe("News item UUID"),
|
id: z.string().uuid().describe("News item UUID"),
|
||||||
},
|
},
|
||||||
async ({ id }) => {
|
handleTool(async ({ id }) => {
|
||||||
const data = await client.post<unknown>(`/api/news/${id}/submit`);
|
const data = await client.post<unknown>(`/api/news/${id}/submit`);
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -114,25 +127,25 @@ export function registerNewsTools(server: McpServer, client: BetterNewsClient) {
|
|||||||
"Edit a news item. Owners can only edit draft/needs_revision items. Mod/Admin can edit any. Requires news:write scope.",
|
"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"),
|
id: z.string().uuid().describe("News item UUID"),
|
||||||
title: z.string().min(1).max(200).optional(),
|
title: z.string().min(1).max(200).optional().describe("Article title"),
|
||||||
readMoreUrl: z.string().url().optional(),
|
readMoreUrl: z.string().url().optional().describe("URL to the original article"),
|
||||||
sourceId: z.string().uuid().optional(),
|
sourceId: z.string().uuid().optional().describe("Source UUID"),
|
||||||
summary: z.string().max(1000).optional(),
|
summary: z.string().max(1000).optional().describe("Short summary"),
|
||||||
longDescription: z.string().optional(),
|
longDescription: z.string().optional().describe("Detailed description"),
|
||||||
authorsThought: z.string().max(500).optional(),
|
authorsThought: z.string().max(500).optional().describe("Author commentary"),
|
||||||
category: z.string().optional(),
|
category: z.string().optional().describe("Category slug"),
|
||||||
tags: z.array(z.string()).optional(),
|
tags: z.array(z.string()).optional().describe("Array of tag strings"),
|
||||||
language: z.string().optional(),
|
language: z.string().optional().describe("ISO 639-1 language code"),
|
||||||
country: z.string().optional(),
|
country: z.string().optional().describe("ISO 3166-1 alpha-2 country code"),
|
||||||
imageUrl: z.string().url().nullable().optional(),
|
imageUrl: z.string().url().nullable().optional().describe("Hero image URL"),
|
||||||
imageAltText: z.string().nullable().optional(),
|
imageAltText: z.string().nullable().optional().describe("Image alt text"),
|
||||||
imageCredits: z.string().nullable().optional(),
|
imageCredits: z.string().nullable().optional().describe("Image attribution"),
|
||||||
datePublished: z.string().datetime().nullable().optional(),
|
datePublished: z.string().datetime().nullable().optional().describe("Original publication date (ISO 8601)"),
|
||||||
},
|
},
|
||||||
async ({ id, ...body }) => {
|
handleTool(async ({ id, ...body }) => {
|
||||||
const data = await client.patch<unknown>(`/api/news/${id}`, body);
|
const data = await client.patch<unknown>(`/api/news/${id}`, body);
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -143,10 +156,10 @@ export function registerNewsTools(server: McpServer, client: BetterNewsClient) {
|
|||||||
action: z.enum(["approve", "reject", "needs_revision"]).describe("Review decision"),
|
action: z.enum(["approve", "reject", "needs_revision"]).describe("Review decision"),
|
||||||
comment: z.string().optional().describe("Optional feedback comment"),
|
comment: z.string().optional().describe("Optional feedback comment"),
|
||||||
},
|
},
|
||||||
async ({ id, ...body }) => {
|
handleTool(async ({ id, ...body }) => {
|
||||||
const data = await client.post<unknown>(`/api/news/${id}/review`, body);
|
const data = await client.post<unknown>(`/api/news/${id}/review`, body);
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -155,10 +168,10 @@ export function registerNewsTools(server: McpServer, client: BetterNewsClient) {
|
|||||||
{
|
{
|
||||||
id: z.string().uuid().describe("News item UUID"),
|
id: z.string().uuid().describe("News item UUID"),
|
||||||
},
|
},
|
||||||
async ({ id }) => {
|
handleTool(async ({ id }) => {
|
||||||
const data = await client.post<unknown>(`/api/news/${id}/like`);
|
const data = await client.post<unknown>(`/api/news/${id}/like`);
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -167,10 +180,10 @@ export function registerNewsTools(server: McpServer, client: BetterNewsClient) {
|
|||||||
{
|
{
|
||||||
id: z.string().uuid().describe("News item UUID"),
|
id: z.string().uuid().describe("News item UUID"),
|
||||||
},
|
},
|
||||||
async ({ id }) => {
|
handleTool(async ({ id }) => {
|
||||||
const data = await client.post<unknown>(`/api/news/${id}/dislike`);
|
const data = await client.post<unknown>(`/api/news/${id}/dislike`);
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -179,10 +192,10 @@ export function registerNewsTools(server: McpServer, client: BetterNewsClient) {
|
|||||||
{
|
{
|
||||||
id: z.string().uuid().describe("News item UUID"),
|
id: z.string().uuid().describe("News item UUID"),
|
||||||
},
|
},
|
||||||
async ({ id }) => {
|
handleTool(async ({ id }) => {
|
||||||
const data = await client.post<unknown>(`/api/news/${id}/bookmark`);
|
const data = await client.post<unknown>(`/api/news/${id}/bookmark`);
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -191,21 +204,21 @@ export function registerNewsTools(server: McpServer, client: BetterNewsClient) {
|
|||||||
{
|
{
|
||||||
id: z.string().uuid().describe("News item UUID"),
|
id: z.string().uuid().describe("News item UUID"),
|
||||||
},
|
},
|
||||||
async ({ id }) => {
|
handleTool(async ({ id }) => {
|
||||||
const data = await client.delete<unknown>(`/api/news/${id}`);
|
const data = await client.delete<unknown>(`/api/news/${id}`);
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
"get_news_item_stats",
|
"get_news_item_stats",
|
||||||
"Get engagement statistics for a news item. Requires Mod/Admin role.",
|
"Get engagement statistics for a news item. Accessible by the item's own author or Mod/Admin. Requires news:read scope.",
|
||||||
{
|
{
|
||||||
id: z.string().uuid().describe("News item UUID"),
|
id: z.string().uuid().describe("News item UUID"),
|
||||||
},
|
},
|
||||||
async ({ id }) => {
|
handleTool(async ({ id }) => {
|
||||||
const data = await client.get<unknown>(`/api/news/${id}/stats`);
|
const data = await client.get<unknown>(`/api/news/${id}/stats`);
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-30
@@ -1,6 +1,7 @@
|
|||||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import type { BetterNewsClient } from "../client.js";
|
import type { BetterNewsClient } from "../client.js";
|
||||||
|
import { handleTool } from "../utils.js";
|
||||||
|
|
||||||
export function registerSourceTools(server: McpServer, client: BetterNewsClient) {
|
export function registerSourceTools(server: McpServer, client: BetterNewsClient) {
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -12,10 +13,10 @@ export function registerSourceTools(server: McpServer, client: BetterNewsClient)
|
|||||||
search: z.string().optional().describe("Text search over source name"),
|
search: z.string().optional().describe("Text search over source name"),
|
||||||
type: z.string().optional().describe("Filter by source type"),
|
type: z.string().optional().describe("Filter by source type"),
|
||||||
},
|
},
|
||||||
async (args) => {
|
handleTool(async (args) => {
|
||||||
const data = await client.get<unknown>("/api/sources", args);
|
const data = await client.get<unknown>("/api/sources", args);
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -24,10 +25,10 @@ export function registerSourceTools(server: McpServer, client: BetterNewsClient)
|
|||||||
{
|
{
|
||||||
id: z.string().uuid().describe("Source UUID"),
|
id: z.string().uuid().describe("Source UUID"),
|
||||||
},
|
},
|
||||||
async ({ id }) => {
|
handleTool(async ({ id }) => {
|
||||||
const data = await client.get<unknown>(`/api/sources/${id}`);
|
const data = await client.get<unknown>(`/api/sources/${id}`);
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -44,10 +45,10 @@ export function registerSourceTools(server: McpServer, client: BetterNewsClient)
|
|||||||
contactEmail: z.string().email().optional().describe("Contact email"),
|
contactEmail: z.string().email().optional().describe("Contact email"),
|
||||||
logoUrl: z.string().url().optional().describe("Logo image URL"),
|
logoUrl: z.string().url().optional().describe("Logo image URL"),
|
||||||
},
|
},
|
||||||
async (args) => {
|
handleTool(async (args) => {
|
||||||
const data = await client.post<unknown>("/api/sources", args);
|
const data = await client.post<unknown>("/api/sources", args);
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -55,21 +56,21 @@ export function registerSourceTools(server: McpServer, client: BetterNewsClient)
|
|||||||
"Update a news source. Requires Mod/Admin role and sources:write scope.",
|
"Update a news source. Requires Mod/Admin role and sources:write scope.",
|
||||||
{
|
{
|
||||||
id: z.string().uuid().describe("Source UUID"),
|
id: z.string().uuid().describe("Source UUID"),
|
||||||
name: z.string().min(1).max(100).optional(),
|
name: z.string().min(1).max(100).optional().describe("Source name"),
|
||||||
type: z.string().optional(),
|
type: z.string().optional().describe("Source type (e.g. newspaper, blog, wire)"),
|
||||||
url: z.string().url().nullable().optional(),
|
url: z.string().url().nullable().optional().describe("Source homepage URL"),
|
||||||
description: z.string().nullable().optional(),
|
description: z.string().nullable().optional().describe("Short description"),
|
||||||
credibilityScore: z.number().min(0).max(100).nullable().optional(),
|
credibilityScore: z.number().min(0).max(100).nullable().optional().describe("Credibility score 0–100"),
|
||||||
country: z.string().nullable().optional(),
|
country: z.string().nullable().optional().describe("ISO 3166-1 alpha-2 country code"),
|
||||||
language: z.string().nullable().optional(),
|
language: z.string().nullable().optional().describe("ISO 639-1 language code"),
|
||||||
contactEmail: z.string().email().nullable().optional(),
|
contactEmail: z.string().email().nullable().optional().describe("Contact email"),
|
||||||
logoUrl: z.string().url().nullable().optional(),
|
logoUrl: z.string().url().nullable().optional().describe("Logo image URL"),
|
||||||
hidden: z.boolean().optional().describe("Hide/unhide this source"),
|
hidden: z.boolean().optional().describe("Hide/unhide this source"),
|
||||||
},
|
},
|
||||||
async ({ id, ...body }) => {
|
handleTool(async ({ id, ...body }) => {
|
||||||
const data = await client.patch<unknown>(`/api/sources/${id}`, body);
|
const data = await client.patch<unknown>(`/api/sources/${id}`, body);
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -78,10 +79,10 @@ export function registerSourceTools(server: McpServer, client: BetterNewsClient)
|
|||||||
{
|
{
|
||||||
newsItemId: z.string().uuid().describe("News item UUID"),
|
newsItemId: z.string().uuid().describe("News item UUID"),
|
||||||
},
|
},
|
||||||
async ({ newsItemId }) => {
|
handleTool(async ({ newsItemId }) => {
|
||||||
const data = await client.get<unknown>(`/api/news/${newsItemId}/sources`);
|
const data = await client.get<unknown>(`/api/news/${newsItemId}/sources`);
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -93,10 +94,10 @@ export function registerSourceTools(server: McpServer, client: BetterNewsClient)
|
|||||||
title: z.string().optional().describe("Optional title for the source link"),
|
title: z.string().optional().describe("Optional title for the source link"),
|
||||||
description: z.string().optional().describe("Optional description"),
|
description: z.string().optional().describe("Optional description"),
|
||||||
},
|
},
|
||||||
async ({ newsItemId, ...body }) => {
|
handleTool(async ({ newsItemId, ...body }) => {
|
||||||
const data = await client.post<unknown>(`/api/news/${newsItemId}/sources`, body);
|
const data = await client.post<unknown>(`/api/news/${newsItemId}/sources`, body);
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -108,12 +109,12 @@ export function registerSourceTools(server: McpServer, client: BetterNewsClient)
|
|||||||
action: z.enum(["approve", "reject"]).describe("Review decision"),
|
action: z.enum(["approve", "reject"]).describe("Review decision"),
|
||||||
comment: z.string().optional().describe("Optional feedback"),
|
comment: z.string().optional().describe("Optional feedback"),
|
||||||
},
|
},
|
||||||
async ({ newsItemId, submissionId, ...body }) => {
|
handleTool(async ({ newsItemId, submissionId, ...body }) => {
|
||||||
const data = await client.post<unknown>(
|
const data = await client.post<unknown>(
|
||||||
`/api/news/${newsItemId}/sources/${submissionId}/review`,
|
`/api/news/${newsItemId}/sources/${submissionId}/review`,
|
||||||
body,
|
body,
|
||||||
);
|
);
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-18
@@ -1,20 +1,21 @@
|
|||||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import type { BetterNewsClient } from "../client.js";
|
import type { BetterNewsClient } from "../client.js";
|
||||||
|
import { handleTool } from "../utils.js";
|
||||||
|
|
||||||
export function registerTicketTools(server: McpServer, client: BetterNewsClient) {
|
export function registerTicketTools(server: McpServer, client: BetterNewsClient) {
|
||||||
server.tool(
|
server.tool(
|
||||||
"list_my_tickets",
|
"list_my_tickets",
|
||||||
"List the authenticated user's support tickets. Requires tickets:read scope.",
|
"List the authenticated user's support tickets. Requires tickets:read scope.",
|
||||||
{
|
{
|
||||||
page: z.number().int().positive().optional(),
|
page: z.number().int().positive().optional().describe("Page number (default 1)"),
|
||||||
limit: z.number().int().min(1).max(50).optional(),
|
limit: z.number().int().min(1).max(50).optional().describe("Items per page (default 20)"),
|
||||||
status: z.enum(["open", "in_progress", "resolved", "closed"]).optional(),
|
status: z.enum(["open", "in_progress", "resolved", "closed"]).optional().describe("Filter by status"),
|
||||||
},
|
},
|
||||||
async (args) => {
|
handleTool(async (args) => {
|
||||||
const data = await client.get<unknown>("/api/tickets", args);
|
const data = await client.get<unknown>("/api/tickets", args);
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -23,10 +24,10 @@ export function registerTicketTools(server: McpServer, client: BetterNewsClient)
|
|||||||
{
|
{
|
||||||
id: z.string().uuid().describe("Ticket UUID"),
|
id: z.string().uuid().describe("Ticket UUID"),
|
||||||
},
|
},
|
||||||
async ({ id }) => {
|
handleTool(async ({ id }) => {
|
||||||
const data = await client.get<unknown>(`/api/tickets/${id}`);
|
const data = await client.get<unknown>(`/api/tickets/${id}`);
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -37,10 +38,10 @@ export function registerTicketTools(server: McpServer, client: BetterNewsClient)
|
|||||||
description: z.string().min(1).max(5000).describe("Detailed description of the issue"),
|
description: z.string().min(1).max(5000).describe("Detailed description of the issue"),
|
||||||
priority: z.enum(["low", "medium", "high", "critical"]).describe("Ticket priority"),
|
priority: z.enum(["low", "medium", "high", "critical"]).describe("Ticket priority"),
|
||||||
},
|
},
|
||||||
async (body) => {
|
handleTool(async (body) => {
|
||||||
const data = await client.post<unknown>("/api/tickets", body);
|
const data = await client.post<unknown>("/api/tickets", body);
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -50,10 +51,10 @@ export function registerTicketTools(server: McpServer, client: BetterNewsClient)
|
|||||||
id: z.string().uuid().describe("Ticket UUID"),
|
id: z.string().uuid().describe("Ticket UUID"),
|
||||||
content: z.string().min(1).max(5000).describe("Reply text"),
|
content: z.string().min(1).max(5000).describe("Reply text"),
|
||||||
},
|
},
|
||||||
async ({ id, content }) => {
|
handleTool(async ({ id, content }) => {
|
||||||
const data = await client.post<unknown>(`/api/tickets/${id}/reply`, { content });
|
const data = await client.post<unknown>(`/api/tickets/${id}/reply`, { content });
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -63,9 +64,9 @@ export function registerTicketTools(server: McpServer, client: BetterNewsClient)
|
|||||||
id: z.string().uuid().describe("Ticket UUID"),
|
id: z.string().uuid().describe("Ticket UUID"),
|
||||||
status: z.enum(["open", "in_progress", "resolved", "closed"]).describe("New status"),
|
status: z.enum(["open", "in_progress", "resolved", "closed"]).describe("New status"),
|
||||||
},
|
},
|
||||||
async ({ id, status }) => {
|
handleTool(async ({ id, status }) => {
|
||||||
const data = await client.patch<unknown>(`/api/tickets/${id}/status`, { status });
|
const data = await client.patch<unknown>(`/api/tickets/${id}/status`, { status });
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-21
@@ -1,16 +1,17 @@
|
|||||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import type { BetterNewsClient } from "../client.js";
|
import type { BetterNewsClient } from "../client.js";
|
||||||
|
import { handleTool } from "../utils.js";
|
||||||
|
|
||||||
export function registerUserTools(server: McpServer, client: BetterNewsClient) {
|
export function registerUserTools(server: McpServer, client: BetterNewsClient) {
|
||||||
server.tool(
|
server.tool(
|
||||||
"get_current_user",
|
"get_current_user",
|
||||||
"Get the authenticated user's profile and account details. Requires user:profile scope.",
|
"Get the authenticated user's profile and account details. Requires user:profile scope.",
|
||||||
{},
|
{},
|
||||||
async () => {
|
handleTool(async () => {
|
||||||
const data = await client.get<unknown>("/api/user/whoami");
|
const data = await client.get<unknown>("/api/user/whoami");
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -19,10 +20,10 @@ export function registerUserTools(server: McpServer, client: BetterNewsClient) {
|
|||||||
{
|
{
|
||||||
username: z.string().describe("Username to look up"),
|
username: z.string().describe("Username to look up"),
|
||||||
},
|
},
|
||||||
async ({ username }) => {
|
handleTool(async ({ username }) => {
|
||||||
const data = await client.get<unknown>(`/api/user/profile/${username}`);
|
const data = await client.get<unknown>(`/api/users/${username}`);
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -36,20 +37,20 @@ export function registerUserTools(server: McpServer, client: BetterNewsClient) {
|
|||||||
.optional()
|
.optional()
|
||||||
.describe("Filter by status"),
|
.describe("Filter by status"),
|
||||||
},
|
},
|
||||||
async (args) => {
|
handleTool(async (args) => {
|
||||||
const data = await client.get<unknown>("/api/user/news", args);
|
const data = await client.get<unknown>("/api/user/news", args);
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
"list_my_source_submissions",
|
"list_my_source_submissions",
|
||||||
"List the authenticated user's own source submissions. Requires user:profile scope.",
|
"List the authenticated user's own source submissions. Requires user:profile scope.",
|
||||||
{},
|
{},
|
||||||
async () => {
|
handleTool(async () => {
|
||||||
const data = await client.get<unknown>("/api/user/source-submissions");
|
const data = await client.get<unknown>("/api/user/source-submissions");
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -59,10 +60,10 @@ export function registerUserTools(server: McpServer, client: BetterNewsClient) {
|
|||||||
page: z.number().int().positive().optional().describe("Page number (default 1)"),
|
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(100).optional().describe("Items per page (default 20)"),
|
||||||
},
|
},
|
||||||
async (args) => {
|
handleTool(async (args) => {
|
||||||
const data = await client.get<unknown>("/api/user/bookmarks", args);
|
const data = await client.get<unknown>("/api/user/bookmarks", args);
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
@@ -75,12 +76,12 @@ export function registerUserTools(server: McpServer, client: BetterNewsClient) {
|
|||||||
avatarUrl: z.string().url().nullable().optional().describe("Avatar image URL"),
|
avatarUrl: z.string().url().nullable().optional().describe("Avatar image URL"),
|
||||||
profilePublic: z.boolean().optional().describe("Whether the profile is public"),
|
profilePublic: z.boolean().optional().describe("Whether the profile is public"),
|
||||||
showEmail: z.boolean().optional().describe("Whether to show email on profile"),
|
showEmail: z.boolean().optional().describe("Whether to show email on profile"),
|
||||||
emailNewsUpdates: z.boolean().optional(),
|
emailNewsUpdates: z.boolean().optional().describe("Opt in/out of news update emails"),
|
||||||
emailCommentEvents: z.boolean().optional(),
|
emailCommentEvents: z.boolean().optional().describe("Opt in/out of comment notification emails"),
|
||||||
},
|
},
|
||||||
async (body) => {
|
handleTool(async (body) => {
|
||||||
const data = await client.patch<unknown>("/api/user/profile", body);
|
const data = await client.patch<unknown>("/api/user/profile", body);
|
||||||
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
return JSON.stringify(data, null, 2);
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
type TextContent = { type: "text"; text: string };
|
||||||
|
type ToolResult = { content: TextContent[]; isError?: boolean };
|
||||||
|
|
||||||
|
export function handleTool<T>(
|
||||||
|
fn: (args: T) => Promise<string>,
|
||||||
|
): (args: T) => Promise<ToolResult> {
|
||||||
|
return async (args: T) => {
|
||||||
|
try {
|
||||||
|
const text = await fn(args);
|
||||||
|
return { content: [{ type: "text", text }] };
|
||||||
|
} catch (err) {
|
||||||
|
const text = err instanceof Error ? err.message : String(err);
|
||||||
|
return { isError: true, content: [{ type: "text", text }] };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user