Skip to content

Commit 62577bd

Browse files
exportcomments-botclaude
andcommitted
feat(mcp): Phase 3 — 17 new tools (picker, account, webhooks, schedules)
Bringing the MCP server's tool count from 6 to 23. Lets MCP clients manage the full account surface — exports, picker, webhooks, recurring schedules — without leaving chat. New tools: - pick_random_winners — FTC-compliant giveaway picker. Downloads the JSON export, optionally filters by @-mention / #hashtag / substring, dedupes by username (Fisher-Yates shuffle). - get_my_profile / get_my_quota / get_my_limits — read-only account info wrapped from /api/v1/user/{profile,quota,limits}. - list_webhooks, create_webhook, update_webhook, delete_webhook, toggle_webhook, test_webhook — 6 webhook CRUD tools. - list_schedules, create_schedule, update_schedule, delete_schedule, run_schedule, pause_schedule, resume_schedule — 7 scheduled-export tools. Client changes: - Smart auth header: JWT-shaped tokens go on Authorization: Bearer, legacy API tokens stay on X-AUTH-TOKEN. This is the seamless story for OAuth-issued tokens that the new HTTP transport receives. - v1Request<T>() helper for the /api/v1 surface (user, webhooks, schedules). /api/v3 stays the export surface. Smoke tested: tools/list via stdio + HTTP both return all 23 tools. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 773b063 commit 62577bd

2 files changed

Lines changed: 442 additions & 1 deletion

File tree

‎src/client.ts‎

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,14 @@ import type {
88
} from './types.js';
99

1010
const BASE_URL = 'https://exportcomments.com/api/v3';
11+
const V1_BASE_URL = 'https://exportcomments.com/api/v1';
1112
const USER_AGENT = 'exportcomments-cli/1.0.0';
1213

14+
/** A JWT has three base64url-encoded segments separated by dots (header.payload.signature). */
15+
function looksLikeJwt(token: string): boolean {
16+
return /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(token);
17+
}
18+
1319
export class ExportCommentsClient {
1420
private token: string;
1521
private baseUrl: string;
@@ -20,13 +26,56 @@ export class ExportCommentsClient {
2026
}
2127

2228
private get headers(): Record<string, string> {
29+
const auth: Record<string, string> = looksLikeJwt(this.token)
30+
? { Authorization: `Bearer ${this.token}` }
31+
: { 'X-AUTH-TOKEN': this.token };
2332
return {
24-
'X-AUTH-TOKEN': this.token,
33+
...auth,
2534
'Content-Type': 'application/json',
2635
'User-Agent': USER_AGENT,
2736
};
2837
}
2938

39+
/** Make a request against /api/v1/* (most user/webhook/schedule endpoints). */
40+
private async v1Request<T>(
41+
method: string,
42+
path: string,
43+
body?: unknown
44+
): Promise<CLIOutput<T>> {
45+
const v1BaseUrl = this.baseUrl.replace('/v3', '/v1');
46+
const url = `${v1BaseUrl}${path}`;
47+
try {
48+
const res = await fetch(url, {
49+
method,
50+
headers: this.headers,
51+
body: body ? JSON.stringify(body) : undefined,
52+
});
53+
const text = await res.text();
54+
let data: T | ApiError;
55+
try {
56+
data = JSON.parse(text);
57+
} catch {
58+
if (!res.ok) {
59+
return { ok: false, error: `HTTP ${res.status}: ${res.statusText}`, detail: text.slice(0, 500) };
60+
}
61+
return { ok: true, data: text as unknown as T };
62+
}
63+
if (!res.ok) {
64+
const err = data as ApiError;
65+
return {
66+
ok: false,
67+
error: err.error ?? `HTTP ${res.status}`,
68+
error_code: err.error_code,
69+
detail: err.detail,
70+
};
71+
}
72+
return { ok: true, data: data as T };
73+
} catch (err: unknown) {
74+
const message = err instanceof Error ? err.message : String(err);
75+
return { ok: false, error: message };
76+
}
77+
}
78+
3079
private async request<T>(
3180
method: string,
3281
path: string,
@@ -204,6 +253,90 @@ export class ExportCommentsClient {
204253
}
205254
}
206255

256+
// ───────────────────────────────────────────────────────────────────────
257+
// /api/v1/* — user account, webhooks, scheduled exports
258+
// ───────────────────────────────────────────────────────────────────────
259+
260+
/** Authenticated user's profile (email, tier, account info). */
261+
async getProfile(): Promise<CLIOutput<unknown>> {
262+
return this.v1Request('GET', '/user/profile');
263+
}
264+
265+
/** Current usage quota: daily/monthly counts vs. allowances. */
266+
async getQuota(): Promise<CLIOutput<unknown>> {
267+
return this.v1Request('GET', '/user/quota');
268+
}
269+
270+
/** Plan tier limits (concurrent exports, max comments, rate limits, etc.). */
271+
async getLimits(): Promise<CLIOutput<unknown>> {
272+
return this.v1Request('GET', '/user/limits');
273+
}
274+
275+
/** List all webhooks for the authenticated account. */
276+
async listWebhooks(): Promise<CLIOutput<unknown>> {
277+
return this.v1Request('GET', '/webhooks');
278+
}
279+
280+
/**
281+
* @param event subscription event name (e.g. "export.completed")
282+
* @param url POST target — must respond 2xx within 10s
283+
*/
284+
async createWebhook(event: string, url: string): Promise<CLIOutput<unknown>> {
285+
return this.v1Request('POST', '/webhooks', { event, url });
286+
}
287+
288+
async updateWebhook(uuid: string, fields: { event?: string; url?: string }): Promise<CLIOutput<unknown>> {
289+
return this.v1Request('PATCH', `/webhooks/${uuid}`, fields);
290+
}
291+
292+
async deleteWebhook(uuid: string): Promise<CLIOutput<unknown>> {
293+
return this.v1Request('DELETE', `/webhooks/${uuid}`);
294+
}
295+
296+
async toggleWebhook(uuid: string): Promise<CLIOutput<unknown>> {
297+
return this.v1Request('POST', `/webhooks/${uuid}/toggle`);
298+
}
299+
300+
/** Sends a test event payload to a webhook URL for connectivity verification. */
301+
async testWebhook(uuid: string): Promise<CLIOutput<unknown>> {
302+
return this.v1Request('POST', `/webhooks/${uuid}/test`);
303+
}
304+
305+
// ── Scheduled exports ──
306+
307+
async listSchedules(): Promise<CLIOutput<unknown>> {
308+
return this.v1Request('GET', '/schedules');
309+
}
310+
311+
async createSchedule(payload: {
312+
url: string;
313+
cron?: string;
314+
frequency?: string;
315+
options?: Record<string, unknown>;
316+
}): Promise<CLIOutput<unknown>> {
317+
return this.v1Request('POST', '/schedules', payload);
318+
}
319+
320+
async updateSchedule(uuid: string, fields: Record<string, unknown>): Promise<CLIOutput<unknown>> {
321+
return this.v1Request('PATCH', `/schedules/${uuid}`, fields);
322+
}
323+
324+
async deleteSchedule(uuid: string): Promise<CLIOutput<unknown>> {
325+
return this.v1Request('DELETE', `/schedules/${uuid}`);
326+
}
327+
328+
async runSchedule(uuid: string): Promise<CLIOutput<unknown>> {
329+
return this.v1Request('POST', `/schedules/${uuid}/run`);
330+
}
331+
332+
async pauseSchedule(uuid: string): Promise<CLIOutput<unknown>> {
333+
return this.v1Request('POST', `/schedules/${uuid}/pause`);
334+
}
335+
336+
async resumeSchedule(uuid: string): Promise<CLIOutput<unknown>> {
337+
return this.v1Request('POST', `/schedules/${uuid}/resume`);
338+
}
339+
207340
/** Ping the API to check connectivity */
208341
async ping(): Promise<CLIOutput<{ status: string }>> {
209342
// Ping uses v1 endpoint

0 commit comments

Comments
 (0)