caelario-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # Caelario MCP
2
+
3
+ The official local MCP server for the Caelario public social-data API. It gives
4
+ Codex and Claude Code typed tools for capability discovery, immediate and
5
+ durable extraction, batches, result pagination, credit estimates, and schedules.
6
+
7
+ ## Install for Codex
8
+
9
+ ```bash
10
+ npx -y caelario-mcp install codex
11
+ ```
12
+
13
+ ## Install for Claude Code
14
+
15
+ ```bash
16
+ npx -y caelario-mcp install claude
17
+ ```
18
+
19
+ The installer prompts for a Caelario workspace API key and registers the stdio
20
+ server at user scope. It does not ask for Instagram, TikTok, YouTube, X, or
21
+ Facebook credentials; provider identities are managed by Caelario.
22
+
23
+ After installation, restart the client and ask:
24
+
25
+ > Use Caelario to get Nike's Instagram profile and 10 recent posts.
26
+
27
+ The package requires Node.js 20 or newer. See
28
+ <https://caelario.com/mcp-server> for security, billing, and tool details.
29
+
30
+ The npm artifact also includes official MCP Registry metadata under
31
+ `io.github.chunkydonut21/caelario`. npm publication must happen before the
32
+ separate Registry publication step.
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { installClient, printHelp } from "../src/install.js";
4
+ import { runServer } from "../src/server.js";
5
+
6
+ const [command = "serve", client] = process.argv.slice(2);
7
+
8
+ try {
9
+ if (command === "serve") {
10
+ await runServer();
11
+ } else if (command === "install" && (client === "codex" || client === "claude")) {
12
+ await installClient(client);
13
+ } else if (command === "--help" || command === "-h" || command === "help") {
14
+ printHelp();
15
+ } else {
16
+ printHelp();
17
+ process.exitCode = 2;
18
+ }
19
+ } catch (error) {
20
+ process.stderr.write(`Caelario MCP: ${error instanceof Error ? error.message : String(error)}\n`);
21
+ process.exitCode = 1;
22
+ }
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "caelario-mcp",
3
+ "version": "0.1.0",
4
+ "mcpName": "io.github.chunkydonut21/caelario",
5
+ "description": "Official Caelario MCP server for public social data",
6
+ "type": "module",
7
+ "license": "UNLICENSED",
8
+ "homepage": "https://caelario.com/mcp-server",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/chunkydonut21/caelario.git",
12
+ "directory": "packages/caelario-mcp"
13
+ },
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "bin": {
18
+ "caelario-mcp": "bin/caelario-mcp.js"
19
+ },
20
+ "files": [
21
+ "bin",
22
+ "src",
23
+ "README.md",
24
+ "server.json"
25
+ ],
26
+ "engines": {
27
+ "node": ">=20"
28
+ },
29
+ "scripts": {
30
+ "test": "node --test test/*.test.js",
31
+ "pack:check": "npm pack --dry-run --cache .npm-cache"
32
+ },
33
+ "dependencies": {
34
+ "@modelcontextprotocol/server": "^2.0.0",
35
+ "zod": "^4.1.0"
36
+ },
37
+ "devDependencies": {
38
+ "@modelcontextprotocol/client": "^2.0.0"
39
+ },
40
+ "keywords": [
41
+ "caelario",
42
+ "mcp",
43
+ "model-context-protocol",
44
+ "social-data",
45
+ "instagram",
46
+ "tiktok",
47
+ "youtube",
48
+ "facebook",
49
+ "twitter",
50
+ "codex",
51
+ "claude"
52
+ ]
53
+ }
package/server.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.chunkydonut21/caelario",
4
+ "title": "Caelario",
5
+ "description": "Public social-data extraction, discovery, batches, schedules, results, and usage tools for Caelario.",
6
+ "repository": {
7
+ "url": "https://github.com/chunkydonut21/caelario",
8
+ "source": "github"
9
+ },
10
+ "version": "0.1.0",
11
+ "packages": [
12
+ {
13
+ "registryType": "npm",
14
+ "identifier": "caelario-mcp",
15
+ "version": "0.1.0",
16
+ "transport": {
17
+ "type": "stdio"
18
+ },
19
+ "environmentVariables": [
20
+ {
21
+ "description": "A revocable Caelario workspace API key from the Developers page.",
22
+ "isRequired": true,
23
+ "format": "string",
24
+ "isSecret": true,
25
+ "name": "CAELARIO_API_KEY"
26
+ }
27
+ ]
28
+ }
29
+ ]
30
+ }
package/src/install.js ADDED
@@ -0,0 +1,105 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import process from "node:process";
3
+ import readline from "node:readline";
4
+
5
+ const CLIENTS = new Set(["codex", "claude"]);
6
+ const KEY_PATTERN = /^[A-Za-z0-9._-]{16,256}$/;
7
+
8
+ export function registrationCommand(client, apiKey, apiUrl = "https://api.caelario.com", platform = process.platform) {
9
+ if (!CLIENTS.has(client)) throw new Error("client must be codex or claude");
10
+ if (!KEY_PATTERN.test(apiKey)) throw new Error("the Caelario API key format is invalid");
11
+ const origin = new URL(apiUrl);
12
+ if (origin.protocol !== "https:" && origin.hostname !== "localhost" && origin.hostname !== "127.0.0.1") {
13
+ throw new Error("the Caelario API URL must use HTTPS");
14
+ }
15
+ if (origin.pathname !== "/" || origin.search || origin.hash || origin.username || origin.password) {
16
+ throw new Error("the Caelario API URL must contain only an origin");
17
+ }
18
+
19
+ const launcher = platform === "win32"
20
+ ? ["cmd", "/c", "npx", "-y", "caelario-mcp", "serve"]
21
+ : ["npx", "-y", "caelario-mcp", "serve"];
22
+ const common = [
23
+ "mcp", "add",
24
+ ...(client === "claude" ? ["--scope", "user"] : []),
25
+ "caelario",
26
+ "--env", `CAELARIO_API_URL=${origin.origin}`,
27
+ "--env", `CAELARIO_API_KEY=${apiKey}`,
28
+ "--",
29
+ ...launcher,
30
+ ];
31
+ return { command: client, args: common };
32
+ }
33
+
34
+ export async function installClient(
35
+ client,
36
+ {
37
+ apiKey = process.env.CAELARIO_API_KEY,
38
+ apiUrl = process.env.CAELARIO_API_URL || "https://api.caelario.com",
39
+ platform = process.platform,
40
+ spawn = spawnSync,
41
+ } = {},
42
+ ) {
43
+ if (!CLIENTS.has(client)) throw new Error("client must be codex or claude");
44
+ const key = apiKey?.trim() || await promptSecret("Paste your Caelario API key: ");
45
+ const registration = registrationCommand(client, key, apiUrl, platform);
46
+ const completed = spawn(registration.command, registration.args, {
47
+ stdio: "inherit",
48
+ windowsHide: true,
49
+ shell: platform === "win32",
50
+ });
51
+ if (completed.error?.code === "ENOENT") {
52
+ throw new Error(`${client === "codex" ? "Codex" : "Claude Code"} CLI was not found. Install it, then run this command again.`);
53
+ }
54
+ if (completed.error) throw completed.error;
55
+ if (completed.status !== 0) {
56
+ throw new Error(`${client === "codex" ? "Codex" : "Claude Code"} rejected the MCP registration (exit ${completed.status ?? "unknown"})`);
57
+ }
58
+ process.stderr.write(`Caelario is connected to ${client === "codex" ? "Codex" : "Claude Code"}. Restart the client, then ask it to use Caelario.\n`);
59
+ }
60
+
61
+ async function promptSecret(prompt) {
62
+ if (!process.stdin.isTTY || !process.stderr.isTTY) {
63
+ throw new Error("set CAELARIO_API_KEY when running the installer non-interactively");
64
+ }
65
+ process.stderr.write(prompt);
66
+ process.stdin.setRawMode?.(true);
67
+ process.stdin.resume();
68
+ process.stdin.setEncoding("utf8");
69
+ let value = "";
70
+ return new Promise((resolve, reject) => {
71
+ const finish = (error) => {
72
+ process.stdin.setRawMode?.(false);
73
+ process.stdin.pause();
74
+ process.stdin.removeListener("data", onData);
75
+ process.stderr.write("\n");
76
+ if (error) reject(error); else resolve(value);
77
+ };
78
+ const onData = (chunk) => {
79
+ for (const character of chunk) {
80
+ if (character === "\r" || character === "\n") return finish();
81
+ if (character === "\u0003") return finish(new Error("installation cancelled"));
82
+ if (character === "\u007f" || character === "\b") {
83
+ value = value.slice(0, -1);
84
+ continue;
85
+ }
86
+ value += character;
87
+ }
88
+ };
89
+ process.stdin.on("data", onData);
90
+ });
91
+ }
92
+
93
+ export function printHelp() {
94
+ const output = [
95
+ "Caelario MCP",
96
+ "",
97
+ " npx -y caelario-mcp install codex Register for Codex (user-wide)",
98
+ " npx -y caelario-mcp install claude Register for Claude Code (user-wide)",
99
+ " npx -y caelario-mcp serve Run the stdio server",
100
+ "",
101
+ "The guided installer securely prompts for CAELARIO_API_KEY.",
102
+ ];
103
+ readline.clearLine(process.stderr, 0);
104
+ process.stderr.write(`${output.join("\n")}\n`);
105
+ }
package/src/server.js ADDED
@@ -0,0 +1,195 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import process from "node:process";
3
+ import { McpServer } from "@modelcontextprotocol/server";
4
+ import { serveStdio } from "@modelcontextprotocol/server/stdio";
5
+ import * as z from "zod/v4";
6
+
7
+ const TERMINAL = new Set(["completed", "failed", "cancelled"]);
8
+ const operation = z.string().min(1).max(64).describe("Operation returned by discover_platforms");
9
+ const platform = z.string().min(1).max(32).describe("Platform returned by discover_platforms");
10
+ const id = z.string().regex(/^[A-Za-z0-9_-]{1,128}$/);
11
+
12
+ function apiConfiguration() {
13
+ const apiKey = (process.env.CAELARIO_API_KEY || "").trim();
14
+ if (!apiKey) throw new Error("CAELARIO_API_KEY is required; run `npx -y caelario-mcp install codex` or `install claude`");
15
+ const baseUrl = new URL(process.env.CAELARIO_API_URL || "https://api.caelario.com");
16
+ if (baseUrl.protocol !== "https:" && !["localhost", "127.0.0.1", "::1"].includes(baseUrl.hostname)) {
17
+ throw new Error("CAELARIO_API_URL requires HTTPS except on loopback");
18
+ }
19
+ if (baseUrl.pathname !== "/" || baseUrl.search || baseUrl.hash || baseUrl.username || baseUrl.password) {
20
+ throw new Error("CAELARIO_API_URL must contain only an API origin");
21
+ }
22
+ return { apiKey, baseUrl: baseUrl.origin };
23
+ }
24
+
25
+ async function api(method, path, { body, params, idempotencyKey } = {}) {
26
+ const { apiKey, baseUrl } = apiConfiguration();
27
+ const url = new URL(path, `${baseUrl}/`);
28
+ for (const [key, value] of Object.entries(params || {})) {
29
+ if (value !== undefined && value !== null) url.searchParams.set(key, String(value));
30
+ }
31
+ const response = await fetch(url, {
32
+ method,
33
+ redirect: "manual",
34
+ headers: {
35
+ Accept: "application/json",
36
+ "Content-Type": "application/json",
37
+ "X-API-Key": apiKey,
38
+ ...(idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}),
39
+ },
40
+ body: body === undefined ? undefined : JSON.stringify(body),
41
+ signal: AbortSignal.timeout(Number(process.env.CAELARIO_MCP_TIMEOUT_MS || 30_000)),
42
+ });
43
+ const payload = response.status === 204 ? null : await response.json().catch(() => null);
44
+ if (!response.ok) {
45
+ const detail = payload?.detail;
46
+ const code = typeof detail === "object" ? detail?.error : undefined;
47
+ const message = typeof detail === "object" ? detail?.message : typeof detail === "string" ? detail : undefined;
48
+ throw new Error(`Caelario API request failed (${response.status}${code ? `, ${code}` : ""})${message ? `: ${message}` : ""}`);
49
+ }
50
+ return payload;
51
+ }
52
+
53
+ function response(value) {
54
+ return {
55
+ content: [{ type: "text", text: JSON.stringify(value) }],
56
+ structuredContent: { result: value },
57
+ };
58
+ }
59
+
60
+ function withoutResult(job) {
61
+ if (!job || typeof job !== "object") return job;
62
+ const { result: _result, ...summary } = job;
63
+ return summary;
64
+ }
65
+
66
+ function capabilityFor(catalog, platformName, operationName) {
67
+ const definition = catalog.find((item) => item.platform === platformName.toLowerCase());
68
+ const capability = definition?.operations?.find((item) => item.operation === operationName);
69
+ if (!definition || !capability) throw new Error(`Unsupported operation: ${platformName}/${operationName}`);
70
+ return { definition, capability };
71
+ }
72
+
73
+ function billing(capability) {
74
+ if (capability.base_credits && !capability.credits_per_result) {
75
+ return `${capability.base_credits} credits for a non-empty collection; valid empty results cost 0.`;
76
+ }
77
+ if (capability.results_per_credit > 1) {
78
+ return `${capability.credits_per_result} credits per ${capability.results_per_credit} delivered results, rounded up.`;
79
+ }
80
+ return `${capability.credits_per_result} credit${capability.credits_per_result === 1 ? "" : "s"} per delivered result.`;
81
+ }
82
+
83
+ function pagedResult(job, offset, limit, textOffset, textLimit) {
84
+ const page = (value) => Array.isArray(value) ? value.slice(offset, offset + limit) : value;
85
+ let result = job.result;
86
+ const pagination = {};
87
+ if (Array.isArray(result)) {
88
+ pagination.result = pageInfo(result.length, offset, limit);
89
+ result = page(result);
90
+ } else if (result && typeof result === "object") {
91
+ result = { ...result };
92
+ for (const [key, value] of Object.entries(result)) {
93
+ if (Array.isArray(value)) {
94
+ pagination[key] = pageInfo(value.length, offset, limit);
95
+ result[key] = page(value);
96
+ } else if (typeof value === "string" && (textOffset || value.length > textLimit)) {
97
+ pagination[key] = pageInfo(value.length, textOffset, textLimit);
98
+ result[key] = value.slice(textOffset, textOffset + textLimit);
99
+ }
100
+ }
101
+ }
102
+ return { job_id: job.id, status: job.status, ready: job.status === "completed" && job.result != null, result_count: job.result_count, billed_credits: job.billed_credits, source_exhausted: Boolean(job.source_exhausted), next_cursor: job.next_cursor ?? null, result, pagination };
103
+ }
104
+
105
+ function pageInfo(total, offset, limit) {
106
+ const returned = Math.max(0, Math.min(limit, total - offset));
107
+ return { offset, limit, returned, total, next_offset: offset + returned < total ? offset + returned : null };
108
+ }
109
+
110
+ function requestBody(input) {
111
+ return { platform: input.platform, operation: input.operation, target: input.target, limit: input.limit, include_posts: input.include_posts, cursor: input.cursor ?? null };
112
+ }
113
+
114
+ const extractionSchema = z.object({
115
+ platform,
116
+ operation,
117
+ target: z.string().min(1).max(2_000),
118
+ limit: z.number().int().min(1).max(25_000).default(12),
119
+ include_posts: z.number().int().min(0).max(200).default(0),
120
+ cursor: z.string().max(8_000).optional(),
121
+ idempotency_key: z.string().min(1).max(128).optional(),
122
+ });
123
+
124
+ export function createServer() {
125
+ const server = new McpServer(
126
+ { name: "Caelario", version: "0.1.0" },
127
+ { instructions: "Use discover_platforms and get_operation_guide before unfamiliar or expensive work. Prefer extract_now for one bounded target. Use durable jobs or batches for larger work, then wait and page the result. Caelario manages provider sessions internally; never ask users for social-network credentials." },
128
+ );
129
+ const tool = (name, description, inputSchema, handler) => server.registerTool(name, { description, inputSchema }, async (input) => response(await handler(input)));
130
+
131
+ tool("discover_platforms", "List every Caelario platform, operation, limit, price, and provider-session requirement.", z.object({}), () => api("GET", "/v1/platforms"));
132
+ tool("get_operation_guide", "Explain and validate one operation before spending credits.", z.object({ platform, operation }), async (input) => {
133
+ const catalog = await api("GET", "/v1/platforms");
134
+ const { capability } = capabilityFor(catalog, input.platform, input.operation);
135
+ return { platform: input.platform.toLowerCase(), operation: input.operation, capability, billing: billing(capability), provider_session: capability.requires_session ? "Managed internally by Caelario; do not supply social credentials." : "No customer social login is required." };
136
+ });
137
+ tool("get_account", "Return workspace identity and extraction limits.", z.object({}), () => api("GET", "/v1/account"));
138
+ tool("get_credit_usage", "Return available, reserved, promotional, purchased, and spent credits.", z.object({}), () => api("GET", "/v1/usage/current"));
139
+ tool("estimate_credits", "Estimate the maximum reservation for one or many targets before starting work.", z.object({ platform, operation, limit: z.number().int().min(1).max(25_000).default(12), include_posts: z.number().int().min(0).max(200).default(0), target_count: z.number().int().min(1).max(100).default(1) }), async (input) => {
140
+ const catalog = await api("GET", "/v1/platforms");
141
+ const { definition, capability } = capabilityFor(catalog, input.platform, input.operation);
142
+ const single = new Set(["profile", "post", "video", "transcript", "adlibrary_ad", "adlibrary_ad_transcript", "profile_analytics", "playlist"]);
143
+ const fixedCollection = input.platform.toLowerCase() === "instagram" && new Set(["highlights", "highlight_items", "stories"]).has(input.operation);
144
+ const normalizedLimit = single.has(input.operation) ? 1 : fixedCollection ? capability.max_results : input.limit;
145
+ if (normalizedLimit > capability.max_results) throw new Error(`limit exceeds the platform maximum of ${capability.max_results}`);
146
+ if (input.include_posts && input.operation !== "profile") throw new Error("include_posts is supported only for profile requests");
147
+ const posts = input.include_posts ? definition.operations.find((item) => item.operation === "posts") : null;
148
+ if (input.include_posts > (posts?.max_results || 0)) throw new Error(`include_posts exceeds the platform maximum of ${posts?.max_results || 0}`);
149
+ // Profile bundles use the backend's included_credit_rate. Standalone post
150
+ // list grouping does not apply to posts embedded in a profile request.
151
+ const included = input.include_posts * (posts?.credits_per_result || 0);
152
+ const perTarget = capability.base_credits + Math.ceil(normalizedLimit * capability.credits_per_result / Math.max(1, capability.results_per_credit)) + Math.ceil(included);
153
+ return { ...input, normalized_limit: normalizedLimit, maximum_reserved_credits_per_target: perTarget, maximum_reserved_credits_total: perTarget * input.target_count, settlement: "Actual billing uses delivered results and releases unused reservation." };
154
+ });
155
+ tool("extract_now", "Run one bounded target synchronously and return a model-safe result page.", extractionSchema.extend({ result_offset: z.number().int().min(0).default(0), result_limit: z.number().int().min(1).max(100).default(50), text_offset: z.number().int().min(0).default(0), text_limit: z.number().int().min(1).max(50_000).default(20_000) }), async (input) => {
156
+ const idempotencyKey = input.idempotency_key || randomUUID();
157
+ const job = await api("POST", "/v1/extract", { body: requestBody(input), idempotencyKey });
158
+ return { idempotency_key: idempotencyKey, ...pagedResult(job, input.result_offset, input.result_limit, input.text_offset, input.text_limit) };
159
+ });
160
+ tool("create_extraction", "Create one durable extraction job for work that may continue in the background.", extractionSchema, async (input) => {
161
+ const idempotencyKey = input.idempotency_key || randomUUID();
162
+ return { idempotency_key: idempotencyKey, job: withoutResult(await api("POST", "/v1/jobs", { body: requestBody(input), idempotencyKey })) };
163
+ });
164
+ tool("list_extractions", "List recent durable extraction jobs without embedding their large results.", z.object({ limit: z.number().int().min(1).max(100).default(25) }), (input) => api("GET", "/v1/jobs", { params: input }));
165
+ tool("get_extraction", "Get one job's progress, billing, and error metadata.", z.object({ job_id: id }), async ({ job_id }) => withoutResult(await api("GET", `/v1/jobs/${job_id}`)));
166
+ tool("get_extraction_result", "Page through a completed job's structured result without flooding model context.", z.object({ job_id: id, offset: z.number().int().min(0).default(0), limit: z.number().int().min(1).max(100).default(50), text_offset: z.number().int().min(0).default(0), text_limit: z.number().int().min(1).max(50_000).default(20_000) }), async (input) => pagedResult(await api("GET", `/v1/jobs/${input.job_id}`), input.offset, input.limit, input.text_offset, input.text_limit));
167
+ tool("wait_for_extraction", "Wait briefly for a durable job to become terminal without returning its large result.", z.object({ job_id: id, timeout_seconds: z.number().min(1).max(120).default(60), poll_interval_seconds: z.number().min(.5).max(10).default(2) }), async (input) => {
168
+ const deadline = Date.now() + input.timeout_seconds * 1_000;
169
+ while (true) {
170
+ const job = await api("GET", `/v1/jobs/${input.job_id}`);
171
+ if (TERMINAL.has(job.status)) return { timed_out: false, job: withoutResult(job) };
172
+ if (Date.now() >= deadline) return { timed_out: true, job: withoutResult(job) };
173
+ await new Promise((resolve) => setTimeout(resolve, input.poll_interval_seconds * 1_000));
174
+ }
175
+ });
176
+ tool("cancel_extraction", "Cancel a queued job or request cancellation of a running job.", z.object({ job_id: id }), ({ job_id }) => api("POST", `/v1/jobs/${job_id}/cancel`));
177
+ tool("create_batch", "Run one operation for up to 100 targets as an expandable durable batch.", z.object({ platform, operation, targets: z.array(z.string().min(1).max(2_000)).min(1).max(100), limit: z.number().int().min(1).max(25_000).default(12), include_posts: z.number().int().min(0).max(200).default(0), idempotency_key: z.string().min(1).max(128).optional() }), async ({ idempotency_key, ...input }) => {
178
+ const idempotencyKey = idempotency_key || randomUUID();
179
+ return { idempotency_key: idempotencyKey, batch: await api("POST", "/v1/batches", { body: input, idempotencyKey }) };
180
+ });
181
+ tool("list_batches", "List recent batches and optionally their child jobs.", z.object({ limit: z.number().int().min(1).max(100).default(25), include_jobs: z.boolean().default(false) }), (input) => api("GET", "/v1/batches", { params: input }));
182
+ tool("get_batch", "Get aggregate status and child jobs for one batch.", z.object({ batch_id: id }), ({ batch_id }) => api("GET", `/v1/batches/${batch_id}`));
183
+ tool("cancel_batch", "Cancel queued batch children and request cancellation of running children.", z.object({ batch_id: id }), ({ batch_id }) => api("POST", `/v1/batches/${batch_id}/cancel`));
184
+ tool("list_schedules", "List active and paused extraction schedules.", z.object({ limit: z.number().int().min(1).max(100).default(50) }), (input) => api("GET", "/v1/schedules", { params: input }));
185
+ tool("get_schedule", "Get one schedule and its recent run history.", z.object({ schedule_id: id }), ({ schedule_id }) => api("GET", `/v1/schedules/${schedule_id}`));
186
+ const scheduleSchema = z.object({ name: z.string().min(1).max(120), platform, operation, targets: z.array(z.string().min(1).max(2_000)).min(1).max(100), frequency: z.enum(["once", "daily", "weekly", "monthly"]), timezone: z.string().min(1).max(100), starts_at: z.iso.datetime({ offset: true }), limit: z.number().int().min(1).max(25_000).default(12), include_posts: z.number().int().min(0).max(200).default(0) });
187
+ tool("create_schedule", "Create a timezone-aware once, daily, weekly, or monthly extraction schedule.", scheduleSchema, (input) => api("POST", "/v1/schedules", { body: { ...input, overlap_policy: "skip" } }));
188
+ tool("update_schedule", "Replace a schedule definition while preserving its run history.", scheduleSchema.extend({ schedule_id: id }), ({ schedule_id, ...input }) => api("PUT", `/v1/schedules/${schedule_id}`, { body: { ...input, overlap_policy: "skip" } }));
189
+ tool("manage_schedule", "Pause, resume, run now, or delete an extraction schedule.", z.object({ schedule_id: id, action: z.enum(["pause", "resume", "run", "delete"]) }), ({ schedule_id, action }) => action === "delete" ? api("DELETE", `/v1/schedules/${schedule_id}`) : api("POST", `/v1/schedules/${schedule_id}/${action}`));
190
+ return server;
191
+ }
192
+
193
+ export async function runServer() {
194
+ await serveStdio(createServer);
195
+ }