@cronwatch/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,37 @@
1
+ # @cronwatch/mcp
2
+
3
+ An MCP server for apps that use [`@cronwatch/sdk`](https://www.npmjs.com/package/@cronwatch/sdk). It talks to the JSON API the app mounts with `cw.routes()`, so Claude Code, Cursor or any MCP client can list jobs, read a failing run's error and output, run a check, and silence a job during a fix.
4
+
5
+ ```bash
6
+ claude mcp add cronwatch \
7
+ -e CRONWATCH_URL=https://app.example.com/cronwatch \
8
+ -e CRONWATCH_TOKEN=your-token \
9
+ -- npx -y @cronwatch/mcp
10
+ ```
11
+
12
+ Any stdio MCP client:
13
+
14
+ ```json
15
+ {
16
+ "mcpServers": {
17
+ "cronwatch": {
18
+ "command": "npx",
19
+ "args": ["-y", "@cronwatch/mcp"],
20
+ "env": { "CRONWATCH_URL": "https://app.example.com/cronwatch", "CRONWATCH_TOKEN": "your-token" }
21
+ }
22
+ }
23
+ }
24
+ ```
25
+
26
+ ## Tools
27
+
28
+ | Tool | |
29
+ |---|---|
30
+ | `list_jobs` | every job with health, schedule, last run and next due |
31
+ | `get_job` | one job with recent runs: errors, output tails, metrics |
32
+ | `run_check` | look for missed and stuck runs now and send due alerts |
33
+ | `silence_job`, `unsilence_job` | pause and resume alerts for a job |
34
+ | `forget_job` | remove a job that no longer exists in the code |
35
+ | `get_setup_guide` | the code to add CronWatch to a job |
36
+
37
+ The token grants everything the dashboard can do. Docs: [cronwatch.dev/docs/mcp](https://cronwatch.dev/docs/mcp/). MIT.
package/dist/cli.js ADDED
@@ -0,0 +1,285 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+
6
+ // src/server.ts
7
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+ import { z } from "zod";
9
+
10
+ // src/client.ts
11
+ var ApiError = class extends Error {
12
+ constructor(message, status) {
13
+ super(message);
14
+ this.status = status;
15
+ this.name = "ApiError";
16
+ }
17
+ status;
18
+ };
19
+ var ApiClient = class {
20
+ base;
21
+ token;
22
+ fetchFn;
23
+ constructor(options) {
24
+ this.base = options.baseUrl.replace(/\/+$/, "");
25
+ this.token = options.token;
26
+ this.fetchFn = options.fetch ?? fetch;
27
+ }
28
+ async call(method, path, body) {
29
+ const headers = { accept: "application/json" };
30
+ if (this.token) headers.authorization = `Bearer ${this.token}`;
31
+ if (body !== void 0) headers["content-type"] = "application/json";
32
+ const response = await this.fetchFn(`${this.base}/api${path}`, {
33
+ method,
34
+ headers,
35
+ body: body === void 0 ? void 0 : JSON.stringify(body)
36
+ });
37
+ const text = await response.text();
38
+ let data = null;
39
+ try {
40
+ data = text ? JSON.parse(text) : null;
41
+ } catch {
42
+ throw new ApiError(`${method} ${path} answered ${response.status} with a non-JSON body: ${text.slice(0, 200)}`, response.status);
43
+ }
44
+ if (!response.ok) {
45
+ const message = typeof data === "object" && data && "error" in data ? String(data.error) : response.statusText;
46
+ throw new ApiError(`${method} ${path} failed (${response.status}): ${message}`, response.status);
47
+ }
48
+ return data;
49
+ }
50
+ };
51
+
52
+ // src/server.ts
53
+ var VERSION = "0.1.0";
54
+ function iso(at) {
55
+ return at === null ? "never" : new Date(at).toISOString();
56
+ }
57
+ function ms(v) {
58
+ if (v === null) return "?";
59
+ if (v < 1e3) return `${v}ms`;
60
+ if (v < 6e4) return `${(v / 1e3).toFixed(1)}s`;
61
+ return `${Math.round(v / 6e4)}m`;
62
+ }
63
+ function summarizeJob(j) {
64
+ const bits = [
65
+ `${j.name}: ${j.health}${j.open.length ? ` (open: ${j.open.join(", ")})` : ""}`,
66
+ ` schedule: ${j.definition.schedule ?? "none"}`,
67
+ ` last run: ${j.lastRun ? j.lastRun.status === "running" ? `running since ${iso(j.lastRun.startedAt)}` : `${j.lastRun.status} at ${iso(j.lastRun.startedAt)}, ${ms(j.lastRun.durationMs)}` : "never"}`,
68
+ ` next due: ${iso(j.nextExpectedAt)}`,
69
+ ` last ${j.stats.runs} runs: ${Math.round(j.stats.okRate * 100)}% ok, p50 ${ms(j.stats.p50Ms)}, p95 ${ms(j.stats.p95Ms)}`
70
+ ];
71
+ if (j.consecutiveFailures) bits.push(` consecutive failures: ${j.consecutiveFailures}`);
72
+ if (j.silencedUntil && j.silencedUntil > Date.now()) bits.push(` silenced until ${iso(j.silencedUntil)}`);
73
+ if (j.definition.description) bits.push(` ${j.definition.description}`);
74
+ return bits.join("\n");
75
+ }
76
+ function summarizeRun(r) {
77
+ const head = `${r.status} at ${iso(r.startedAt)}, ${ms(r.durationMs)}, trigger ${r.trigger}, id ${r.id}`;
78
+ const parts = [head];
79
+ if (Object.keys(r.metrics).length) parts.push(` metrics: ${JSON.stringify(r.metrics)}`);
80
+ if (r.error) parts.push(` error: ${r.error.split("\n").slice(0, 6).join("\n ")}`);
81
+ if (r.output) {
82
+ const lines = r.output.trimEnd().split("\n");
83
+ parts.push(` output (${lines.length} lines, tail):
84
+ ${lines.slice(-12).join("\n ")}`);
85
+ }
86
+ return parts.join("\n");
87
+ }
88
+ var SETUP_GUIDE = `# Adding CronWatch to a job
89
+
90
+ CronWatch is a library, not a service. Install @cronwatch/sdk, declare each job once, wrap the work, and mount the routes.
91
+
92
+ \`\`\`ts
93
+ // lib/cronwatch.ts
94
+ import { cronwatch } from "@cronwatch/sdk";
95
+ import { sqlite } from "@cronwatch/sdk/sqlite"; // or "@cronwatch/sdk/postgres"
96
+ import { slack } from "@cronwatch/sdk/slack"; // or discord, webhook
97
+
98
+ export const cw = cronwatch({
99
+ store: sqlite({ path: "./data/cronwatch.db" }),
100
+ alerts: [slack({ webhookUrl: process.env.SLACK_WEBHOOK_URL! })],
101
+ });
102
+
103
+ export const nightlyReport = cw.job("nightly-report", {
104
+ schedule: "0 2 * * *", // cron, "@hourly", or "every 15m"
105
+ timezone: "UTC", // Vercel and GitHub Actions crons run in UTC
106
+ grace: "15m", // how late a start may be before it is "missed"
107
+ timeout: "30m", // a run still going after this is "stuck"
108
+ expect: "Report written", // the output must contain this, or the run failed
109
+ budget: { cost: 2 }, // a run reporting cost above 2 is "over budget"
110
+ });
111
+ \`\`\`
112
+
113
+ \`\`\`ts
114
+ // app/api/cron/nightly-report/route.ts (Vercel cron hits this; Authorization: Bearer CRON_SECRET is checked)
115
+ import { nightlyReport } from "@/lib/cronwatch";
116
+ export const GET = nightlyReport.handler(async (job) => {
117
+ const result = await buildReport();
118
+ job.log("Report written:", result.path);
119
+ job.metric("cost", result.usdCost);
120
+ });
121
+ \`\`\`
122
+
123
+ \`\`\`ts
124
+ // app/cronwatch/[[...path]]/route.ts (dashboard + JSON API; set CRONWATCH_TOKEN)
125
+ import { cw } from "@/lib/cronwatch";
126
+ export const { GET, POST, DELETE } = cw.routes();
127
+ \`\`\`
128
+
129
+ Missed runs are found by \`cw.check()\`. Either call \`cw.start()\` once in a long-running server (instrumentation.ts in Next.js), or point a cron at \`GET /cronwatch/api/check\` every few minutes with the bearer token.
130
+
131
+ For a plain function outside HTTP: \`await nightlyReport.run(async (job) => { ... })\`. The function's exception is recorded as a failure and rethrown.
132
+
133
+ Docs: https://cronwatch.dev/docs`;
134
+ function createServer(options) {
135
+ const api = new ApiClient({ baseUrl: options.baseUrl, token: options.token, fetch: options.fetch });
136
+ const server2 = new McpServer({ name: "cronwatch", version: VERSION });
137
+ const text = (t) => ({ content: [{ type: "text", text: t }] });
138
+ const failure = (e) => ({
139
+ content: [{ type: "text", text: e instanceof ApiError ? e.message : `Error: ${e.message}` }],
140
+ isError: true
141
+ });
142
+ server2.registerTool(
143
+ "list_jobs",
144
+ {
145
+ title: "List jobs",
146
+ description: "Every scheduled job CronWatch knows about in this app, with its health (healthy, late, failing, stuck, silenced, never_ran), schedule, last run and next due time. Start here.",
147
+ inputSchema: {}
148
+ },
149
+ async () => {
150
+ try {
151
+ const { jobs } = await api.call("GET", "/jobs");
152
+ if (jobs.length === 0) return text("No jobs yet. Declare one with cw.job() and run it once; see get_setup_guide.");
153
+ const unhealthy = jobs.filter((j) => j.health !== "healthy");
154
+ const head = `${jobs.length} job${jobs.length === 1 ? "" : "s"}, ${unhealthy.length} needing attention.`;
155
+ return text([head, "", ...jobs.map(summarizeJob)].join("\n"));
156
+ } catch (e) {
157
+ return failure(e);
158
+ }
159
+ }
160
+ );
161
+ server2.registerTool(
162
+ "get_job",
163
+ {
164
+ title: "Get job",
165
+ description: "One job in detail: definition, health, open conditions and its recent runs with errors, output tails and metrics. Use it to work out why a job failed.",
166
+ inputSchema: { name: z.string().describe("The job name"), runs: z.number().int().min(1).max(100).optional().describe("How many recent runs to include (default 10)") }
167
+ },
168
+ async ({ name, runs }) => {
169
+ try {
170
+ const data = await api.call("GET", `/jobs/${encodeURIComponent(name)}?runs=${runs ?? 10}`);
171
+ const def = JSON.stringify(data.job.definition, null, 2);
172
+ return text([summarizeJob(data.job), "", `definition: ${def}`, "", `recent runs (${data.runs.length}):`, ...data.runs.map((r) => summarizeRun(r))].join("\n"));
173
+ } catch (e) {
174
+ return failure(e);
175
+ }
176
+ }
177
+ );
178
+ server2.registerTool(
179
+ "run_check",
180
+ {
181
+ title: "Run check now",
182
+ description: "Look for missed and stuck runs across every job right now and send any alerts that are due. Returns what it found.",
183
+ inputSchema: {}
184
+ },
185
+ async () => {
186
+ try {
187
+ const result = await api.call("POST", "/check");
188
+ const lines = [`Checked ${result.jobs.length} jobs at ${iso(result.checkedAt)}.`];
189
+ if (result.alerts.length) lines.push(`Alerts sent: ${result.alerts.map((a) => `${a.job} ${a.type}`).join(", ")}`);
190
+ else lines.push("No new alerts.");
191
+ const bad = result.jobs.filter((j) => j.health !== "healthy");
192
+ if (bad.length) lines.push("", ...bad.map(summarizeJob));
193
+ return text(lines.join("\n"));
194
+ } catch (e) {
195
+ return failure(e);
196
+ }
197
+ }
198
+ );
199
+ server2.registerTool(
200
+ "silence_job",
201
+ {
202
+ title: "Silence a job",
203
+ description: "Stop alerts for a job for a while, for example during a migration. Runs keep being recorded.",
204
+ inputSchema: { name: z.string(), for: z.string().default("1h").describe('A duration like "30m", "2h", "1d"') }
205
+ },
206
+ async ({ name, for: duration }) => {
207
+ try {
208
+ const { state } = await api.call("POST", `/jobs/${encodeURIComponent(name)}/silence`, { for: duration });
209
+ return text(`${name} is silenced until ${iso(state.silencedUntil)}.`);
210
+ } catch (e) {
211
+ return failure(e);
212
+ }
213
+ }
214
+ );
215
+ server2.registerTool(
216
+ "unsilence_job",
217
+ { title: "Unsilence a job", description: "Resume alerts for a silenced job.", inputSchema: { name: z.string() } },
218
+ async ({ name }) => {
219
+ try {
220
+ await api.call("POST", `/jobs/${encodeURIComponent(name)}/unsilence`);
221
+ return text(`${name} alerts are back on.`);
222
+ } catch (e) {
223
+ return failure(e);
224
+ }
225
+ }
226
+ );
227
+ server2.registerTool(
228
+ "forget_job",
229
+ {
230
+ title: "Forget a job",
231
+ description: "Remove a job and its run history from the store. For jobs that no longer exist in the code. A job still declared in code comes back on its next run.",
232
+ inputSchema: { name: z.string() }
233
+ },
234
+ async ({ name }) => {
235
+ try {
236
+ await api.call("DELETE", `/jobs/${encodeURIComponent(name)}`);
237
+ return text(`${name} removed.`);
238
+ } catch (e) {
239
+ return failure(e);
240
+ }
241
+ }
242
+ );
243
+ server2.registerTool(
244
+ "get_setup_guide",
245
+ {
246
+ title: "How to add a job",
247
+ description: "The code to add CronWatch monitoring to a scheduled job in this app: declaring the job, wrapping a route handler or function, mounting the dashboard, and running the missed-run check. Read it before writing any CronWatch code.",
248
+ inputSchema: {}
249
+ },
250
+ async () => text(SETUP_GUIDE)
251
+ );
252
+ return server2;
253
+ }
254
+
255
+ // src/cli.ts
256
+ var args = process.argv.slice(2);
257
+ var flag = (name) => {
258
+ const i = args.indexOf(name);
259
+ return i >= 0 ? args[i + 1] : void 0;
260
+ };
261
+ if (args.includes("--help") || args.includes("-h")) {
262
+ console.log(`cronwatch-mcp: an MCP server for an app that uses @cronwatch/sdk.
263
+
264
+ CRONWATCH_URL where cw.routes() is mounted, e.g. https://app.example.com/cronwatch
265
+ CRONWATCH_TOKEN the token those routes expect
266
+
267
+ Or pass --url and --token. Add it to Claude Code with:
268
+
269
+ claude mcp add cronwatch -e CRONWATCH_URL=https://app.example.com/cronwatch -e CRONWATCH_TOKEN=... -- npx -y @cronwatch/mcp
270
+ `);
271
+ process.exit(0);
272
+ }
273
+ var baseUrl = flag("--url") ?? process.env.CRONWATCH_URL;
274
+ var token = flag("--token") ?? process.env.CRONWATCH_TOKEN ?? null;
275
+ if (!baseUrl) {
276
+ console.error("cronwatch-mcp: set CRONWATCH_URL (or pass --url) to where cw.routes() is mounted, e.g. https://app.example.com/cronwatch");
277
+ process.exit(2);
278
+ }
279
+ if (!token) {
280
+ console.error("cronwatch-mcp: no CRONWATCH_TOKEN given; this only works if the app's routes are unprotected (development).");
281
+ }
282
+ var server = createServer({ baseUrl, token });
283
+ var transport = new StdioServerTransport();
284
+ await server.connect(transport);
285
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts","../src/server.ts","../src/client.ts"],"sourcesContent":["import { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { createServer } from \"./server.js\";\n\nconst args = process.argv.slice(2);\nconst flag = (name: string): string | undefined => {\n const i = args.indexOf(name);\n return i >= 0 ? args[i + 1] : undefined;\n};\n\nif (args.includes(\"--help\") || args.includes(\"-h\")) {\n console.log(`cronwatch-mcp: an MCP server for an app that uses @cronwatch/sdk.\n\n CRONWATCH_URL where cw.routes() is mounted, e.g. https://app.example.com/cronwatch\n CRONWATCH_TOKEN the token those routes expect\n\nOr pass --url and --token. Add it to Claude Code with:\n\n claude mcp add cronwatch -e CRONWATCH_URL=https://app.example.com/cronwatch -e CRONWATCH_TOKEN=... -- npx -y @cronwatch/mcp\n`);\n process.exit(0);\n}\n\nconst baseUrl = flag(\"--url\") ?? process.env.CRONWATCH_URL;\nconst token = flag(\"--token\") ?? process.env.CRONWATCH_TOKEN ?? null;\n\nif (!baseUrl) {\n console.error(\"cronwatch-mcp: set CRONWATCH_URL (or pass --url) to where cw.routes() is mounted, e.g. https://app.example.com/cronwatch\");\n process.exit(2);\n}\nif (!token) {\n console.error(\"cronwatch-mcp: no CRONWATCH_TOKEN given; this only works if the app's routes are unprotected (development).\");\n}\n\nconst server = createServer({ baseUrl, token });\nconst transport = new StdioServerTransport();\nawait server.connect(transport);\n","import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\nimport { ApiClient, ApiError } from \"./client.js\";\n\nconst VERSION = \"0.1.0\";\n\nexport interface ServerOptions {\n baseUrl: string;\n token: string | null;\n fetch?: typeof fetch;\n}\n\ninterface Run {\n id: string;\n status: string;\n startedAt: number;\n finishedAt: number | null;\n durationMs: number | null;\n error: string | null;\n output: string | null;\n metrics: Record<string, number>;\n trigger: string;\n}\n\ninterface JobSummary {\n name: string;\n definition: Record<string, unknown> & { schedule?: string; description?: string };\n health: string;\n open: string[];\n lastRun: Run | null;\n nextExpectedAt: number | null;\n consecutiveFailures: number;\n silencedUntil: number | null;\n stats: { runs: number; okRate: number; p50Ms: number | null; p95Ms: number | null };\n}\n\nfunction iso(at: number | null): string {\n return at === null ? \"never\" : new Date(at).toISOString();\n}\n\nfunction ms(v: number | null): string {\n if (v === null) return \"?\";\n if (v < 1000) return `${v}ms`;\n if (v < 60_000) return `${(v / 1000).toFixed(1)}s`;\n return `${Math.round(v / 60_000)}m`;\n}\n\nfunction summarizeJob(j: JobSummary): string {\n const bits = [\n `${j.name}: ${j.health}${j.open.length ? ` (open: ${j.open.join(\", \")})` : \"\"}`,\n ` schedule: ${j.definition.schedule ?? \"none\"}`,\n ` last run: ${j.lastRun ? (j.lastRun.status === \"running\" ? `running since ${iso(j.lastRun.startedAt)}` : `${j.lastRun.status} at ${iso(j.lastRun.startedAt)}, ${ms(j.lastRun.durationMs)}`) : \"never\"}`,\n ` next due: ${iso(j.nextExpectedAt)}`,\n ` last ${j.stats.runs} runs: ${Math.round(j.stats.okRate * 100)}% ok, p50 ${ms(j.stats.p50Ms)}, p95 ${ms(j.stats.p95Ms)}`,\n ];\n if (j.consecutiveFailures) bits.push(` consecutive failures: ${j.consecutiveFailures}`);\n if (j.silencedUntil && j.silencedUntil > Date.now()) bits.push(` silenced until ${iso(j.silencedUntil)}`);\n if (j.definition.description) bits.push(` ${j.definition.description}`);\n return bits.join(\"\\n\");\n}\n\nfunction summarizeRun(r: Run): string {\n const head = `${r.status} at ${iso(r.startedAt)}, ${ms(r.durationMs)}, trigger ${r.trigger}, id ${r.id}`;\n const parts = [head];\n if (Object.keys(r.metrics).length) parts.push(` metrics: ${JSON.stringify(r.metrics)}`);\n if (r.error) parts.push(` error: ${r.error.split(\"\\n\").slice(0, 6).join(\"\\n \")}`);\n if (r.output) {\n const lines = r.output.trimEnd().split(\"\\n\");\n parts.push(` output (${lines.length} lines, tail):\\n ${lines.slice(-12).join(\"\\n \")}`);\n }\n return parts.join(\"\\n\");\n}\n\nconst SETUP_GUIDE = `# Adding CronWatch to a job\n\nCronWatch is a library, not a service. Install @cronwatch/sdk, declare each job once, wrap the work, and mount the routes.\n\n\\`\\`\\`ts\n// lib/cronwatch.ts\nimport { cronwatch } from \"@cronwatch/sdk\";\nimport { sqlite } from \"@cronwatch/sdk/sqlite\"; // or \"@cronwatch/sdk/postgres\"\nimport { slack } from \"@cronwatch/sdk/slack\"; // or discord, webhook\n\nexport const cw = cronwatch({\n store: sqlite({ path: \"./data/cronwatch.db\" }),\n alerts: [slack({ webhookUrl: process.env.SLACK_WEBHOOK_URL! })],\n});\n\nexport const nightlyReport = cw.job(\"nightly-report\", {\n schedule: \"0 2 * * *\", // cron, \"@hourly\", or \"every 15m\"\n timezone: \"UTC\", // Vercel and GitHub Actions crons run in UTC\n grace: \"15m\", // how late a start may be before it is \"missed\"\n timeout: \"30m\", // a run still going after this is \"stuck\"\n expect: \"Report written\", // the output must contain this, or the run failed\n budget: { cost: 2 }, // a run reporting cost above 2 is \"over budget\"\n});\n\\`\\`\\`\n\n\\`\\`\\`ts\n// app/api/cron/nightly-report/route.ts (Vercel cron hits this; Authorization: Bearer CRON_SECRET is checked)\nimport { nightlyReport } from \"@/lib/cronwatch\";\nexport const GET = nightlyReport.handler(async (job) => {\n const result = await buildReport();\n job.log(\"Report written:\", result.path);\n job.metric(\"cost\", result.usdCost);\n});\n\\`\\`\\`\n\n\\`\\`\\`ts\n// app/cronwatch/[[...path]]/route.ts (dashboard + JSON API; set CRONWATCH_TOKEN)\nimport { cw } from \"@/lib/cronwatch\";\nexport const { GET, POST, DELETE } = cw.routes();\n\\`\\`\\`\n\nMissed runs are found by \\`cw.check()\\`. Either call \\`cw.start()\\` once in a long-running server (instrumentation.ts in Next.js), or point a cron at \\`GET /cronwatch/api/check\\` every few minutes with the bearer token.\n\nFor a plain function outside HTTP: \\`await nightlyReport.run(async (job) => { ... })\\`. The function's exception is recorded as a failure and rethrown.\n\nDocs: https://cronwatch.dev/docs`;\n\nexport function createServer(options: ServerOptions): McpServer {\n const api = new ApiClient({ baseUrl: options.baseUrl, token: options.token, fetch: options.fetch });\n const server = new McpServer({ name: \"cronwatch\", version: VERSION });\n\n const text = (t: string) => ({ content: [{ type: \"text\" as const, text: t }] });\n const failure = (e: unknown) => ({\n content: [{ type: \"text\" as const, text: e instanceof ApiError ? e.message : `Error: ${(e as Error).message}` }],\n isError: true,\n });\n\n server.registerTool(\n \"list_jobs\",\n {\n title: \"List jobs\",\n description: \"Every scheduled job CronWatch knows about in this app, with its health (healthy, late, failing, stuck, silenced, never_ran), schedule, last run and next due time. Start here.\",\n inputSchema: {},\n },\n async () => {\n try {\n const { jobs } = await api.call<{ jobs: JobSummary[] }>(\"GET\", \"/jobs\");\n if (jobs.length === 0) return text(\"No jobs yet. Declare one with cw.job() and run it once; see get_setup_guide.\");\n const unhealthy = jobs.filter((j) => j.health !== \"healthy\");\n const head = `${jobs.length} job${jobs.length === 1 ? \"\" : \"s\"}, ${unhealthy.length} needing attention.`;\n return text([head, \"\", ...jobs.map(summarizeJob)].join(\"\\n\"));\n } catch (e) {\n return failure(e);\n }\n },\n );\n\n server.registerTool(\n \"get_job\",\n {\n title: \"Get job\",\n description: \"One job in detail: definition, health, open conditions and its recent runs with errors, output tails and metrics. Use it to work out why a job failed.\",\n inputSchema: { name: z.string().describe(\"The job name\"), runs: z.number().int().min(1).max(100).optional().describe(\"How many recent runs to include (default 10)\") },\n },\n async ({ name, runs }) => {\n try {\n const data = await api.call<{ job: JobSummary; runs: Run[] }>(\"GET\", `/jobs/${encodeURIComponent(name)}?runs=${runs ?? 10}`);\n const def = JSON.stringify(data.job.definition, null, 2);\n return text([summarizeJob(data.job), \"\", `definition: ${def}`, \"\", `recent runs (${data.runs.length}):`, ...data.runs.map((r) => summarizeRun(r))].join(\"\\n\"));\n } catch (e) {\n return failure(e);\n }\n },\n );\n\n server.registerTool(\n \"run_check\",\n {\n title: \"Run check now\",\n description: \"Look for missed and stuck runs across every job right now and send any alerts that are due. Returns what it found.\",\n inputSchema: {},\n },\n async () => {\n try {\n const result = await api.call<{ checkedAt: number; jobs: JobSummary[]; alerts: { type: string; job: string; title: string }[]; pruned: number }>(\"POST\", \"/check\");\n const lines = [`Checked ${result.jobs.length} jobs at ${iso(result.checkedAt)}.`];\n if (result.alerts.length) lines.push(`Alerts sent: ${result.alerts.map((a) => `${a.job} ${a.type}`).join(\", \")}`);\n else lines.push(\"No new alerts.\");\n const bad = result.jobs.filter((j) => j.health !== \"healthy\");\n if (bad.length) lines.push(\"\", ...bad.map(summarizeJob));\n return text(lines.join(\"\\n\"));\n } catch (e) {\n return failure(e);\n }\n },\n );\n\n server.registerTool(\n \"silence_job\",\n {\n title: \"Silence a job\",\n description: \"Stop alerts for a job for a while, for example during a migration. Runs keep being recorded.\",\n inputSchema: { name: z.string(), for: z.string().default(\"1h\").describe('A duration like \"30m\", \"2h\", \"1d\"') },\n },\n async ({ name, for: duration }) => {\n try {\n const { state } = await api.call<{ state: { silencedUntil: number } }>(\"POST\", `/jobs/${encodeURIComponent(name)}/silence`, { for: duration });\n return text(`${name} is silenced until ${iso(state.silencedUntil)}.`);\n } catch (e) {\n return failure(e);\n }\n },\n );\n\n server.registerTool(\n \"unsilence_job\",\n { title: \"Unsilence a job\", description: \"Resume alerts for a silenced job.\", inputSchema: { name: z.string() } },\n async ({ name }) => {\n try {\n await api.call(\"POST\", `/jobs/${encodeURIComponent(name)}/unsilence`);\n return text(`${name} alerts are back on.`);\n } catch (e) {\n return failure(e);\n }\n },\n );\n\n server.registerTool(\n \"forget_job\",\n {\n title: \"Forget a job\",\n description: \"Remove a job and its run history from the store. For jobs that no longer exist in the code. A job still declared in code comes back on its next run.\",\n inputSchema: { name: z.string() },\n },\n async ({ name }) => {\n try {\n await api.call(\"DELETE\", `/jobs/${encodeURIComponent(name)}`);\n return text(`${name} removed.`);\n } catch (e) {\n return failure(e);\n }\n },\n );\n\n server.registerTool(\n \"get_setup_guide\",\n {\n title: \"How to add a job\",\n description: \"The code to add CronWatch monitoring to a scheduled job in this app: declaring the job, wrapping a route handler or function, mounting the dashboard, and running the missed-run check. Read it before writing any CronWatch code.\",\n inputSchema: {},\n },\n async () => text(SETUP_GUIDE),\n );\n\n return server;\n}\n\nexport { SETUP_GUIDE };\n","/** A small HTTP client for the JSON API that @cronwatch/sdk mounts. */\nexport interface ApiClientOptions {\n /** Where the routes are mounted, e.g. https://app.example.com/cronwatch */\n baseUrl: string;\n token: string | null;\n fetch?: typeof fetch;\n}\n\nexport class ApiError extends Error {\n constructor(message: string, readonly status: number) {\n super(message);\n this.name = \"ApiError\";\n }\n}\n\nexport class ApiClient {\n private readonly base: string;\n private readonly token: string | null;\n private readonly fetchFn: typeof fetch;\n\n constructor(options: ApiClientOptions) {\n this.base = options.baseUrl.replace(/\\/+$/, \"\");\n this.token = options.token;\n this.fetchFn = options.fetch ?? fetch;\n }\n\n async call<T = unknown>(method: string, path: string, body?: unknown): Promise<T> {\n const headers: Record<string, string> = { accept: \"application/json\" };\n if (this.token) headers.authorization = `Bearer ${this.token}`;\n if (body !== undefined) headers[\"content-type\"] = \"application/json\";\n const response = await this.fetchFn(`${this.base}/api${path}`, {\n method,\n headers,\n body: body === undefined ? undefined : JSON.stringify(body),\n });\n const text = await response.text();\n let data: unknown = null;\n try {\n data = text ? JSON.parse(text) : null;\n } catch {\n throw new ApiError(`${method} ${path} answered ${response.status} with a non-JSON body: ${text.slice(0, 200)}`, response.status);\n }\n if (!response.ok) {\n const message = typeof data === \"object\" && data && \"error\" in data ? String((data as { error: unknown }).error) : response.statusText;\n throw new ApiError(`${method} ${path} failed (${response.status}): ${message}`, response.status);\n }\n return data as T;\n }\n}\n"],"mappings":";;;AAAA,SAAS,4BAA4B;;;ACArC,SAAS,iBAAiB;AAC1B,SAAS,SAAS;;;ACOX,IAAM,WAAN,cAAuB,MAAM;AAAA,EAClC,YAAY,SAA0B,QAAgB;AACpD,UAAM,OAAO;AADuB;AAEpC,SAAK,OAAO;AAAA,EACd;AAAA,EAHsC;AAIxC;AAEO,IAAM,YAAN,MAAgB;AAAA,EACJ;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAY,SAA2B;AACrC,SAAK,OAAO,QAAQ,QAAQ,QAAQ,QAAQ,EAAE;AAC9C,SAAK,QAAQ,QAAQ;AACrB,SAAK,UAAU,QAAQ,SAAS;AAAA,EAClC;AAAA,EAEA,MAAM,KAAkB,QAAgB,MAAc,MAA4B;AAChF,UAAM,UAAkC,EAAE,QAAQ,mBAAmB;AACrE,QAAI,KAAK,MAAO,SAAQ,gBAAgB,UAAU,KAAK,KAAK;AAC5D,QAAI,SAAS,OAAW,SAAQ,cAAc,IAAI;AAClD,UAAM,WAAW,MAAM,KAAK,QAAQ,GAAG,KAAK,IAAI,OAAO,IAAI,IAAI;AAAA,MAC7D;AAAA,MACA;AAAA,MACA,MAAM,SAAS,SAAY,SAAY,KAAK,UAAU,IAAI;AAAA,IAC5D,CAAC;AACD,UAAM,OAAO,MAAM,SAAS,KAAK;AACjC,QAAI,OAAgB;AACpB,QAAI;AACF,aAAO,OAAO,KAAK,MAAM,IAAI,IAAI;AAAA,IACnC,QAAQ;AACN,YAAM,IAAI,SAAS,GAAG,MAAM,IAAI,IAAI,aAAa,SAAS,MAAM,0BAA0B,KAAK,MAAM,GAAG,GAAG,CAAC,IAAI,SAAS,MAAM;AAAA,IACjI;AACA,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,UAAU,OAAO,SAAS,YAAY,QAAQ,WAAW,OAAO,OAAQ,KAA4B,KAAK,IAAI,SAAS;AAC5H,YAAM,IAAI,SAAS,GAAG,MAAM,IAAI,IAAI,YAAY,SAAS,MAAM,MAAM,OAAO,IAAI,SAAS,MAAM;AAAA,IACjG;AACA,WAAO;AAAA,EACT;AACF;;;AD5CA,IAAM,UAAU;AAgChB,SAAS,IAAI,IAA2B;AACtC,SAAO,OAAO,OAAO,UAAU,IAAI,KAAK,EAAE,EAAE,YAAY;AAC1D;AAEA,SAAS,GAAG,GAA0B;AACpC,MAAI,MAAM,KAAM,QAAO;AACvB,MAAI,IAAI,IAAM,QAAO,GAAG,CAAC;AACzB,MAAI,IAAI,IAAQ,QAAO,IAAI,IAAI,KAAM,QAAQ,CAAC,CAAC;AAC/C,SAAO,GAAG,KAAK,MAAM,IAAI,GAAM,CAAC;AAClC;AAEA,SAAS,aAAa,GAAuB;AAC3C,QAAM,OAAO;AAAA,IACX,GAAG,EAAE,IAAI,KAAK,EAAE,MAAM,GAAG,EAAE,KAAK,SAAS,WAAW,EAAE,KAAK,KAAK,IAAI,CAAC,MAAM,EAAE;AAAA,IAC7E,eAAe,EAAE,WAAW,YAAY,MAAM;AAAA,IAC9C,eAAe,EAAE,UAAW,EAAE,QAAQ,WAAW,YAAY,iBAAiB,IAAI,EAAE,QAAQ,SAAS,CAAC,KAAK,GAAG,EAAE,QAAQ,MAAM,OAAO,IAAI,EAAE,QAAQ,SAAS,CAAC,KAAK,GAAG,EAAE,QAAQ,UAAU,CAAC,KAAM,OAAO;AAAA,IACvM,eAAe,IAAI,EAAE,cAAc,CAAC;AAAA,IACpC,UAAU,EAAE,MAAM,IAAI,UAAU,KAAK,MAAM,EAAE,MAAM,SAAS,GAAG,CAAC,aAAa,GAAG,EAAE,MAAM,KAAK,CAAC,SAAS,GAAG,EAAE,MAAM,KAAK,CAAC;AAAA,EAC1H;AACA,MAAI,EAAE,oBAAqB,MAAK,KAAK,2BAA2B,EAAE,mBAAmB,EAAE;AACvF,MAAI,EAAE,iBAAiB,EAAE,gBAAgB,KAAK,IAAI,EAAG,MAAK,KAAK,oBAAoB,IAAI,EAAE,aAAa,CAAC,EAAE;AACzG,MAAI,EAAE,WAAW,YAAa,MAAK,KAAK,KAAK,EAAE,WAAW,WAAW,EAAE;AACvE,SAAO,KAAK,KAAK,IAAI;AACvB;AAEA,SAAS,aAAa,GAAgB;AACpC,QAAM,OAAO,GAAG,EAAE,MAAM,OAAO,IAAI,EAAE,SAAS,CAAC,KAAK,GAAG,EAAE,UAAU,CAAC,aAAa,EAAE,OAAO,QAAQ,EAAE,EAAE;AACtG,QAAM,QAAQ,CAAC,IAAI;AACnB,MAAI,OAAO,KAAK,EAAE,OAAO,EAAE,OAAQ,OAAM,KAAK,cAAc,KAAK,UAAU,EAAE,OAAO,CAAC,EAAE;AACvF,MAAI,EAAE,MAAO,OAAM,KAAK,YAAY,EAAE,MAAM,MAAM,IAAI,EAAE,MAAM,GAAG,CAAC,EAAE,KAAK,QAAQ,CAAC,EAAE;AACpF,MAAI,EAAE,QAAQ;AACZ,UAAM,QAAQ,EAAE,OAAO,QAAQ,EAAE,MAAM,IAAI;AAC3C,UAAM,KAAK,aAAa,MAAM,MAAM;AAAA,MAAuB,MAAM,MAAM,GAAG,EAAE,KAAK,QAAQ,CAAC,EAAE;AAAA,EAC9F;AACA,SAAO,MAAM,KAAK,IAAI;AACxB;AAEA,IAAM,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA+Cb,SAAS,aAAa,SAAmC;AAC9D,QAAM,MAAM,IAAI,UAAU,EAAE,SAAS,QAAQ,SAAS,OAAO,QAAQ,OAAO,OAAO,QAAQ,MAAM,CAAC;AAClG,QAAMA,UAAS,IAAI,UAAU,EAAE,MAAM,aAAa,SAAS,QAAQ,CAAC;AAEpE,QAAM,OAAO,CAAC,OAAe,EAAE,SAAS,CAAC,EAAE,MAAM,QAAiB,MAAM,EAAE,CAAC,EAAE;AAC7E,QAAM,UAAU,CAAC,OAAgB;AAAA,IAC/B,SAAS,CAAC,EAAE,MAAM,QAAiB,MAAM,aAAa,WAAW,EAAE,UAAU,UAAW,EAAY,OAAO,GAAG,CAAC;AAAA,IAC/G,SAAS;AAAA,EACX;AAEA,EAAAA,QAAO;AAAA,IACL;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,aAAa;AAAA,MACb,aAAa,CAAC;AAAA,IAChB;AAAA,IACA,YAAY;AACV,UAAI;AACF,cAAM,EAAE,KAAK,IAAI,MAAM,IAAI,KAA6B,OAAO,OAAO;AACtE,YAAI,KAAK,WAAW,EAAG,QAAO,KAAK,8EAA8E;AACjH,cAAM,YAAY,KAAK,OAAO,CAAC,MAAM,EAAE,WAAW,SAAS;AAC3D,cAAM,OAAO,GAAG,KAAK,MAAM,OAAO,KAAK,WAAW,IAAI,KAAK,GAAG,KAAK,UAAU,MAAM;AACnF,eAAO,KAAK,CAAC,MAAM,IAAI,GAAG,KAAK,IAAI,YAAY,CAAC,EAAE,KAAK,IAAI,CAAC;AAAA,MAC9D,SAAS,GAAG;AACV,eAAO,QAAQ,CAAC;AAAA,MAClB;AAAA,IACF;AAAA,EACF;AAEA,EAAAA,QAAO;AAAA,IACL;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,aAAa;AAAA,MACb,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,cAAc,GAAG,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,SAAS,8CAA8C,EAAE;AAAA,IACvK;AAAA,IACA,OAAO,EAAE,MAAM,KAAK,MAAM;AACxB,UAAI;AACF,cAAM,OAAO,MAAM,IAAI,KAAuC,OAAO,SAAS,mBAAmB,IAAI,CAAC,SAAS,QAAQ,EAAE,EAAE;AAC3H,cAAM,MAAM,KAAK,UAAU,KAAK,IAAI,YAAY,MAAM,CAAC;AACvD,eAAO,KAAK,CAAC,aAAa,KAAK,GAAG,GAAG,IAAI,eAAe,GAAG,IAAI,IAAI,gBAAgB,KAAK,KAAK,MAAM,MAAM,GAAG,KAAK,KAAK,IAAI,CAAC,MAAM,aAAa,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC;AAAA,MAC/J,SAAS,GAAG;AACV,eAAO,QAAQ,CAAC;AAAA,MAClB;AAAA,IACF;AAAA,EACF;AAEA,EAAAA,QAAO;AAAA,IACL;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,aAAa;AAAA,MACb,aAAa,CAAC;AAAA,IAChB;AAAA,IACA,YAAY;AACV,UAAI;AACF,cAAM,SAAS,MAAM,IAAI,KAAwH,QAAQ,QAAQ;AACjK,cAAM,QAAQ,CAAC,WAAW,OAAO,KAAK,MAAM,YAAY,IAAI,OAAO,SAAS,CAAC,GAAG;AAChF,YAAI,OAAO,OAAO,OAAQ,OAAM,KAAK,gBAAgB,OAAO,OAAO,IAAI,CAAC,MAAM,GAAG,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,EAAE,KAAK,IAAI,CAAC,EAAE;AAAA,YAC3G,OAAM,KAAK,gBAAgB;AAChC,cAAM,MAAM,OAAO,KAAK,OAAO,CAAC,MAAM,EAAE,WAAW,SAAS;AAC5D,YAAI,IAAI,OAAQ,OAAM,KAAK,IAAI,GAAG,IAAI,IAAI,YAAY,CAAC;AACvD,eAAO,KAAK,MAAM,KAAK,IAAI,CAAC;AAAA,MAC9B,SAAS,GAAG;AACV,eAAO,QAAQ,CAAC;AAAA,MAClB;AAAA,IACF;AAAA,EACF;AAEA,EAAAA,QAAO;AAAA,IACL;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,aAAa;AAAA,MACb,aAAa,EAAE,MAAM,EAAE,OAAO,GAAG,KAAK,EAAE,OAAO,EAAE,QAAQ,IAAI,EAAE,SAAS,mCAAmC,EAAE;AAAA,IAC/G;AAAA,IACA,OAAO,EAAE,MAAM,KAAK,SAAS,MAAM;AACjC,UAAI;AACF,cAAM,EAAE,MAAM,IAAI,MAAM,IAAI,KAA2C,QAAQ,SAAS,mBAAmB,IAAI,CAAC,YAAY,EAAE,KAAK,SAAS,CAAC;AAC7I,eAAO,KAAK,GAAG,IAAI,sBAAsB,IAAI,MAAM,aAAa,CAAC,GAAG;AAAA,MACtE,SAAS,GAAG;AACV,eAAO,QAAQ,CAAC;AAAA,MAClB;AAAA,IACF;AAAA,EACF;AAEA,EAAAA,QAAO;AAAA,IACL;AAAA,IACA,EAAE,OAAO,mBAAmB,aAAa,qCAAqC,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE;AAAA,IAChH,OAAO,EAAE,KAAK,MAAM;AAClB,UAAI;AACF,cAAM,IAAI,KAAK,QAAQ,SAAS,mBAAmB,IAAI,CAAC,YAAY;AACpE,eAAO,KAAK,GAAG,IAAI,sBAAsB;AAAA,MAC3C,SAAS,GAAG;AACV,eAAO,QAAQ,CAAC;AAAA,MAClB;AAAA,IACF;AAAA,EACF;AAEA,EAAAA,QAAO;AAAA,IACL;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,aAAa;AAAA,MACb,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE;AAAA,IAClC;AAAA,IACA,OAAO,EAAE,KAAK,MAAM;AAClB,UAAI;AACF,cAAM,IAAI,KAAK,UAAU,SAAS,mBAAmB,IAAI,CAAC,EAAE;AAC5D,eAAO,KAAK,GAAG,IAAI,WAAW;AAAA,MAChC,SAAS,GAAG;AACV,eAAO,QAAQ,CAAC;AAAA,MAClB;AAAA,IACF;AAAA,EACF;AAEA,EAAAA,QAAO;AAAA,IACL;AAAA,IACA;AAAA,MACE,OAAO;AAAA,MACP,aAAa;AAAA,MACb,aAAa,CAAC;AAAA,IAChB;AAAA,IACA,YAAY,KAAK,WAAW;AAAA,EAC9B;AAEA,SAAOA;AACT;;;ADrPA,IAAM,OAAO,QAAQ,KAAK,MAAM,CAAC;AACjC,IAAM,OAAO,CAAC,SAAqC;AACjD,QAAM,IAAI,KAAK,QAAQ,IAAI;AAC3B,SAAO,KAAK,IAAI,KAAK,IAAI,CAAC,IAAI;AAChC;AAEA,IAAI,KAAK,SAAS,QAAQ,KAAK,KAAK,SAAS,IAAI,GAAG;AAClD,UAAQ,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAQb;AACC,UAAQ,KAAK,CAAC;AAChB;AAEA,IAAM,UAAU,KAAK,OAAO,KAAK,QAAQ,IAAI;AAC7C,IAAM,QAAQ,KAAK,SAAS,KAAK,QAAQ,IAAI,mBAAmB;AAEhE,IAAI,CAAC,SAAS;AACZ,UAAQ,MAAM,0HAA0H;AACxI,UAAQ,KAAK,CAAC;AAChB;AACA,IAAI,CAAC,OAAO;AACV,UAAQ,MAAM,6GAA6G;AAC7H;AAEA,IAAM,SAAS,aAAa,EAAE,SAAS,MAAM,CAAC;AAC9C,IAAM,YAAY,IAAI,qBAAqB;AAC3C,MAAM,OAAO,QAAQ,SAAS;","names":["server"]}
@@ -0,0 +1,29 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+
3
+ interface ServerOptions {
4
+ baseUrl: string;
5
+ token: string | null;
6
+ fetch?: typeof fetch;
7
+ }
8
+ declare function createServer(options: ServerOptions): McpServer;
9
+
10
+ /** A small HTTP client for the JSON API that @cronwatch/sdk mounts. */
11
+ interface ApiClientOptions {
12
+ /** Where the routes are mounted, e.g. https://app.example.com/cronwatch */
13
+ baseUrl: string;
14
+ token: string | null;
15
+ fetch?: typeof fetch;
16
+ }
17
+ declare class ApiError extends Error {
18
+ readonly status: number;
19
+ constructor(message: string, status: number);
20
+ }
21
+ declare class ApiClient {
22
+ private readonly base;
23
+ private readonly token;
24
+ private readonly fetchFn;
25
+ constructor(options: ApiClientOptions);
26
+ call<T = unknown>(method: string, path: string, body?: unknown): Promise<T>;
27
+ }
28
+
29
+ export { ApiClient, ApiError, type ServerOptions, createServer };
package/dist/index.js ADDED
@@ -0,0 +1,256 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/server.ts
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { z } from "zod";
6
+
7
+ // src/client.ts
8
+ var ApiError = class extends Error {
9
+ constructor(message, status) {
10
+ super(message);
11
+ this.status = status;
12
+ this.name = "ApiError";
13
+ }
14
+ status;
15
+ };
16
+ var ApiClient = class {
17
+ base;
18
+ token;
19
+ fetchFn;
20
+ constructor(options) {
21
+ this.base = options.baseUrl.replace(/\/+$/, "");
22
+ this.token = options.token;
23
+ this.fetchFn = options.fetch ?? fetch;
24
+ }
25
+ async call(method, path, body) {
26
+ const headers = { accept: "application/json" };
27
+ if (this.token) headers.authorization = `Bearer ${this.token}`;
28
+ if (body !== void 0) headers["content-type"] = "application/json";
29
+ const response = await this.fetchFn(`${this.base}/api${path}`, {
30
+ method,
31
+ headers,
32
+ body: body === void 0 ? void 0 : JSON.stringify(body)
33
+ });
34
+ const text = await response.text();
35
+ let data = null;
36
+ try {
37
+ data = text ? JSON.parse(text) : null;
38
+ } catch {
39
+ throw new ApiError(`${method} ${path} answered ${response.status} with a non-JSON body: ${text.slice(0, 200)}`, response.status);
40
+ }
41
+ if (!response.ok) {
42
+ const message = typeof data === "object" && data && "error" in data ? String(data.error) : response.statusText;
43
+ throw new ApiError(`${method} ${path} failed (${response.status}): ${message}`, response.status);
44
+ }
45
+ return data;
46
+ }
47
+ };
48
+
49
+ // src/server.ts
50
+ var VERSION = "0.1.0";
51
+ function iso(at) {
52
+ return at === null ? "never" : new Date(at).toISOString();
53
+ }
54
+ function ms(v) {
55
+ if (v === null) return "?";
56
+ if (v < 1e3) return `${v}ms`;
57
+ if (v < 6e4) return `${(v / 1e3).toFixed(1)}s`;
58
+ return `${Math.round(v / 6e4)}m`;
59
+ }
60
+ function summarizeJob(j) {
61
+ const bits = [
62
+ `${j.name}: ${j.health}${j.open.length ? ` (open: ${j.open.join(", ")})` : ""}`,
63
+ ` schedule: ${j.definition.schedule ?? "none"}`,
64
+ ` last run: ${j.lastRun ? j.lastRun.status === "running" ? `running since ${iso(j.lastRun.startedAt)}` : `${j.lastRun.status} at ${iso(j.lastRun.startedAt)}, ${ms(j.lastRun.durationMs)}` : "never"}`,
65
+ ` next due: ${iso(j.nextExpectedAt)}`,
66
+ ` last ${j.stats.runs} runs: ${Math.round(j.stats.okRate * 100)}% ok, p50 ${ms(j.stats.p50Ms)}, p95 ${ms(j.stats.p95Ms)}`
67
+ ];
68
+ if (j.consecutiveFailures) bits.push(` consecutive failures: ${j.consecutiveFailures}`);
69
+ if (j.silencedUntil && j.silencedUntil > Date.now()) bits.push(` silenced until ${iso(j.silencedUntil)}`);
70
+ if (j.definition.description) bits.push(` ${j.definition.description}`);
71
+ return bits.join("\n");
72
+ }
73
+ function summarizeRun(r) {
74
+ const head = `${r.status} at ${iso(r.startedAt)}, ${ms(r.durationMs)}, trigger ${r.trigger}, id ${r.id}`;
75
+ const parts = [head];
76
+ if (Object.keys(r.metrics).length) parts.push(` metrics: ${JSON.stringify(r.metrics)}`);
77
+ if (r.error) parts.push(` error: ${r.error.split("\n").slice(0, 6).join("\n ")}`);
78
+ if (r.output) {
79
+ const lines = r.output.trimEnd().split("\n");
80
+ parts.push(` output (${lines.length} lines, tail):
81
+ ${lines.slice(-12).join("\n ")}`);
82
+ }
83
+ return parts.join("\n");
84
+ }
85
+ var SETUP_GUIDE = `# Adding CronWatch to a job
86
+
87
+ CronWatch is a library, not a service. Install @cronwatch/sdk, declare each job once, wrap the work, and mount the routes.
88
+
89
+ \`\`\`ts
90
+ // lib/cronwatch.ts
91
+ import { cronwatch } from "@cronwatch/sdk";
92
+ import { sqlite } from "@cronwatch/sdk/sqlite"; // or "@cronwatch/sdk/postgres"
93
+ import { slack } from "@cronwatch/sdk/slack"; // or discord, webhook
94
+
95
+ export const cw = cronwatch({
96
+ store: sqlite({ path: "./data/cronwatch.db" }),
97
+ alerts: [slack({ webhookUrl: process.env.SLACK_WEBHOOK_URL! })],
98
+ });
99
+
100
+ export const nightlyReport = cw.job("nightly-report", {
101
+ schedule: "0 2 * * *", // cron, "@hourly", or "every 15m"
102
+ timezone: "UTC", // Vercel and GitHub Actions crons run in UTC
103
+ grace: "15m", // how late a start may be before it is "missed"
104
+ timeout: "30m", // a run still going after this is "stuck"
105
+ expect: "Report written", // the output must contain this, or the run failed
106
+ budget: { cost: 2 }, // a run reporting cost above 2 is "over budget"
107
+ });
108
+ \`\`\`
109
+
110
+ \`\`\`ts
111
+ // app/api/cron/nightly-report/route.ts (Vercel cron hits this; Authorization: Bearer CRON_SECRET is checked)
112
+ import { nightlyReport } from "@/lib/cronwatch";
113
+ export const GET = nightlyReport.handler(async (job) => {
114
+ const result = await buildReport();
115
+ job.log("Report written:", result.path);
116
+ job.metric("cost", result.usdCost);
117
+ });
118
+ \`\`\`
119
+
120
+ \`\`\`ts
121
+ // app/cronwatch/[[...path]]/route.ts (dashboard + JSON API; set CRONWATCH_TOKEN)
122
+ import { cw } from "@/lib/cronwatch";
123
+ export const { GET, POST, DELETE } = cw.routes();
124
+ \`\`\`
125
+
126
+ Missed runs are found by \`cw.check()\`. Either call \`cw.start()\` once in a long-running server (instrumentation.ts in Next.js), or point a cron at \`GET /cronwatch/api/check\` every few minutes with the bearer token.
127
+
128
+ For a plain function outside HTTP: \`await nightlyReport.run(async (job) => { ... })\`. The function's exception is recorded as a failure and rethrown.
129
+
130
+ Docs: https://cronwatch.dev/docs`;
131
+ function createServer(options) {
132
+ const api = new ApiClient({ baseUrl: options.baseUrl, token: options.token, fetch: options.fetch });
133
+ const server = new McpServer({ name: "cronwatch", version: VERSION });
134
+ const text = (t) => ({ content: [{ type: "text", text: t }] });
135
+ const failure = (e) => ({
136
+ content: [{ type: "text", text: e instanceof ApiError ? e.message : `Error: ${e.message}` }],
137
+ isError: true
138
+ });
139
+ server.registerTool(
140
+ "list_jobs",
141
+ {
142
+ title: "List jobs",
143
+ description: "Every scheduled job CronWatch knows about in this app, with its health (healthy, late, failing, stuck, silenced, never_ran), schedule, last run and next due time. Start here.",
144
+ inputSchema: {}
145
+ },
146
+ async () => {
147
+ try {
148
+ const { jobs } = await api.call("GET", "/jobs");
149
+ if (jobs.length === 0) return text("No jobs yet. Declare one with cw.job() and run it once; see get_setup_guide.");
150
+ const unhealthy = jobs.filter((j) => j.health !== "healthy");
151
+ const head = `${jobs.length} job${jobs.length === 1 ? "" : "s"}, ${unhealthy.length} needing attention.`;
152
+ return text([head, "", ...jobs.map(summarizeJob)].join("\n"));
153
+ } catch (e) {
154
+ return failure(e);
155
+ }
156
+ }
157
+ );
158
+ server.registerTool(
159
+ "get_job",
160
+ {
161
+ title: "Get job",
162
+ description: "One job in detail: definition, health, open conditions and its recent runs with errors, output tails and metrics. Use it to work out why a job failed.",
163
+ inputSchema: { name: z.string().describe("The job name"), runs: z.number().int().min(1).max(100).optional().describe("How many recent runs to include (default 10)") }
164
+ },
165
+ async ({ name, runs }) => {
166
+ try {
167
+ const data = await api.call("GET", `/jobs/${encodeURIComponent(name)}?runs=${runs ?? 10}`);
168
+ const def = JSON.stringify(data.job.definition, null, 2);
169
+ return text([summarizeJob(data.job), "", `definition: ${def}`, "", `recent runs (${data.runs.length}):`, ...data.runs.map((r) => summarizeRun(r))].join("\n"));
170
+ } catch (e) {
171
+ return failure(e);
172
+ }
173
+ }
174
+ );
175
+ server.registerTool(
176
+ "run_check",
177
+ {
178
+ title: "Run check now",
179
+ description: "Look for missed and stuck runs across every job right now and send any alerts that are due. Returns what it found.",
180
+ inputSchema: {}
181
+ },
182
+ async () => {
183
+ try {
184
+ const result = await api.call("POST", "/check");
185
+ const lines = [`Checked ${result.jobs.length} jobs at ${iso(result.checkedAt)}.`];
186
+ if (result.alerts.length) lines.push(`Alerts sent: ${result.alerts.map((a) => `${a.job} ${a.type}`).join(", ")}`);
187
+ else lines.push("No new alerts.");
188
+ const bad = result.jobs.filter((j) => j.health !== "healthy");
189
+ if (bad.length) lines.push("", ...bad.map(summarizeJob));
190
+ return text(lines.join("\n"));
191
+ } catch (e) {
192
+ return failure(e);
193
+ }
194
+ }
195
+ );
196
+ server.registerTool(
197
+ "silence_job",
198
+ {
199
+ title: "Silence a job",
200
+ description: "Stop alerts for a job for a while, for example during a migration. Runs keep being recorded.",
201
+ inputSchema: { name: z.string(), for: z.string().default("1h").describe('A duration like "30m", "2h", "1d"') }
202
+ },
203
+ async ({ name, for: duration }) => {
204
+ try {
205
+ const { state } = await api.call("POST", `/jobs/${encodeURIComponent(name)}/silence`, { for: duration });
206
+ return text(`${name} is silenced until ${iso(state.silencedUntil)}.`);
207
+ } catch (e) {
208
+ return failure(e);
209
+ }
210
+ }
211
+ );
212
+ server.registerTool(
213
+ "unsilence_job",
214
+ { title: "Unsilence a job", description: "Resume alerts for a silenced job.", inputSchema: { name: z.string() } },
215
+ async ({ name }) => {
216
+ try {
217
+ await api.call("POST", `/jobs/${encodeURIComponent(name)}/unsilence`);
218
+ return text(`${name} alerts are back on.`);
219
+ } catch (e) {
220
+ return failure(e);
221
+ }
222
+ }
223
+ );
224
+ server.registerTool(
225
+ "forget_job",
226
+ {
227
+ title: "Forget a job",
228
+ description: "Remove a job and its run history from the store. For jobs that no longer exist in the code. A job still declared in code comes back on its next run.",
229
+ inputSchema: { name: z.string() }
230
+ },
231
+ async ({ name }) => {
232
+ try {
233
+ await api.call("DELETE", `/jobs/${encodeURIComponent(name)}`);
234
+ return text(`${name} removed.`);
235
+ } catch (e) {
236
+ return failure(e);
237
+ }
238
+ }
239
+ );
240
+ server.registerTool(
241
+ "get_setup_guide",
242
+ {
243
+ title: "How to add a job",
244
+ description: "The code to add CronWatch monitoring to a scheduled job in this app: declaring the job, wrapping a route handler or function, mounting the dashboard, and running the missed-run check. Read it before writing any CronWatch code.",
245
+ inputSchema: {}
246
+ },
247
+ async () => text(SETUP_GUIDE)
248
+ );
249
+ return server;
250
+ }
251
+ export {
252
+ ApiClient,
253
+ ApiError,
254
+ createServer
255
+ };
256
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server.ts","../src/client.ts"],"sourcesContent":["import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\nimport { ApiClient, ApiError } from \"./client.js\";\n\nconst VERSION = \"0.1.0\";\n\nexport interface ServerOptions {\n baseUrl: string;\n token: string | null;\n fetch?: typeof fetch;\n}\n\ninterface Run {\n id: string;\n status: string;\n startedAt: number;\n finishedAt: number | null;\n durationMs: number | null;\n error: string | null;\n output: string | null;\n metrics: Record<string, number>;\n trigger: string;\n}\n\ninterface JobSummary {\n name: string;\n definition: Record<string, unknown> & { schedule?: string; description?: string };\n health: string;\n open: string[];\n lastRun: Run | null;\n nextExpectedAt: number | null;\n consecutiveFailures: number;\n silencedUntil: number | null;\n stats: { runs: number; okRate: number; p50Ms: number | null; p95Ms: number | null };\n}\n\nfunction iso(at: number | null): string {\n return at === null ? \"never\" : new Date(at).toISOString();\n}\n\nfunction ms(v: number | null): string {\n if (v === null) return \"?\";\n if (v < 1000) return `${v}ms`;\n if (v < 60_000) return `${(v / 1000).toFixed(1)}s`;\n return `${Math.round(v / 60_000)}m`;\n}\n\nfunction summarizeJob(j: JobSummary): string {\n const bits = [\n `${j.name}: ${j.health}${j.open.length ? ` (open: ${j.open.join(\", \")})` : \"\"}`,\n ` schedule: ${j.definition.schedule ?? \"none\"}`,\n ` last run: ${j.lastRun ? (j.lastRun.status === \"running\" ? `running since ${iso(j.lastRun.startedAt)}` : `${j.lastRun.status} at ${iso(j.lastRun.startedAt)}, ${ms(j.lastRun.durationMs)}`) : \"never\"}`,\n ` next due: ${iso(j.nextExpectedAt)}`,\n ` last ${j.stats.runs} runs: ${Math.round(j.stats.okRate * 100)}% ok, p50 ${ms(j.stats.p50Ms)}, p95 ${ms(j.stats.p95Ms)}`,\n ];\n if (j.consecutiveFailures) bits.push(` consecutive failures: ${j.consecutiveFailures}`);\n if (j.silencedUntil && j.silencedUntil > Date.now()) bits.push(` silenced until ${iso(j.silencedUntil)}`);\n if (j.definition.description) bits.push(` ${j.definition.description}`);\n return bits.join(\"\\n\");\n}\n\nfunction summarizeRun(r: Run): string {\n const head = `${r.status} at ${iso(r.startedAt)}, ${ms(r.durationMs)}, trigger ${r.trigger}, id ${r.id}`;\n const parts = [head];\n if (Object.keys(r.metrics).length) parts.push(` metrics: ${JSON.stringify(r.metrics)}`);\n if (r.error) parts.push(` error: ${r.error.split(\"\\n\").slice(0, 6).join(\"\\n \")}`);\n if (r.output) {\n const lines = r.output.trimEnd().split(\"\\n\");\n parts.push(` output (${lines.length} lines, tail):\\n ${lines.slice(-12).join(\"\\n \")}`);\n }\n return parts.join(\"\\n\");\n}\n\nconst SETUP_GUIDE = `# Adding CronWatch to a job\n\nCronWatch is a library, not a service. Install @cronwatch/sdk, declare each job once, wrap the work, and mount the routes.\n\n\\`\\`\\`ts\n// lib/cronwatch.ts\nimport { cronwatch } from \"@cronwatch/sdk\";\nimport { sqlite } from \"@cronwatch/sdk/sqlite\"; // or \"@cronwatch/sdk/postgres\"\nimport { slack } from \"@cronwatch/sdk/slack\"; // or discord, webhook\n\nexport const cw = cronwatch({\n store: sqlite({ path: \"./data/cronwatch.db\" }),\n alerts: [slack({ webhookUrl: process.env.SLACK_WEBHOOK_URL! })],\n});\n\nexport const nightlyReport = cw.job(\"nightly-report\", {\n schedule: \"0 2 * * *\", // cron, \"@hourly\", or \"every 15m\"\n timezone: \"UTC\", // Vercel and GitHub Actions crons run in UTC\n grace: \"15m\", // how late a start may be before it is \"missed\"\n timeout: \"30m\", // a run still going after this is \"stuck\"\n expect: \"Report written\", // the output must contain this, or the run failed\n budget: { cost: 2 }, // a run reporting cost above 2 is \"over budget\"\n});\n\\`\\`\\`\n\n\\`\\`\\`ts\n// app/api/cron/nightly-report/route.ts (Vercel cron hits this; Authorization: Bearer CRON_SECRET is checked)\nimport { nightlyReport } from \"@/lib/cronwatch\";\nexport const GET = nightlyReport.handler(async (job) => {\n const result = await buildReport();\n job.log(\"Report written:\", result.path);\n job.metric(\"cost\", result.usdCost);\n});\n\\`\\`\\`\n\n\\`\\`\\`ts\n// app/cronwatch/[[...path]]/route.ts (dashboard + JSON API; set CRONWATCH_TOKEN)\nimport { cw } from \"@/lib/cronwatch\";\nexport const { GET, POST, DELETE } = cw.routes();\n\\`\\`\\`\n\nMissed runs are found by \\`cw.check()\\`. Either call \\`cw.start()\\` once in a long-running server (instrumentation.ts in Next.js), or point a cron at \\`GET /cronwatch/api/check\\` every few minutes with the bearer token.\n\nFor a plain function outside HTTP: \\`await nightlyReport.run(async (job) => { ... })\\`. The function's exception is recorded as a failure and rethrown.\n\nDocs: https://cronwatch.dev/docs`;\n\nexport function createServer(options: ServerOptions): McpServer {\n const api = new ApiClient({ baseUrl: options.baseUrl, token: options.token, fetch: options.fetch });\n const server = new McpServer({ name: \"cronwatch\", version: VERSION });\n\n const text = (t: string) => ({ content: [{ type: \"text\" as const, text: t }] });\n const failure = (e: unknown) => ({\n content: [{ type: \"text\" as const, text: e instanceof ApiError ? e.message : `Error: ${(e as Error).message}` }],\n isError: true,\n });\n\n server.registerTool(\n \"list_jobs\",\n {\n title: \"List jobs\",\n description: \"Every scheduled job CronWatch knows about in this app, with its health (healthy, late, failing, stuck, silenced, never_ran), schedule, last run and next due time. Start here.\",\n inputSchema: {},\n },\n async () => {\n try {\n const { jobs } = await api.call<{ jobs: JobSummary[] }>(\"GET\", \"/jobs\");\n if (jobs.length === 0) return text(\"No jobs yet. Declare one with cw.job() and run it once; see get_setup_guide.\");\n const unhealthy = jobs.filter((j) => j.health !== \"healthy\");\n const head = `${jobs.length} job${jobs.length === 1 ? \"\" : \"s\"}, ${unhealthy.length} needing attention.`;\n return text([head, \"\", ...jobs.map(summarizeJob)].join(\"\\n\"));\n } catch (e) {\n return failure(e);\n }\n },\n );\n\n server.registerTool(\n \"get_job\",\n {\n title: \"Get job\",\n description: \"One job in detail: definition, health, open conditions and its recent runs with errors, output tails and metrics. Use it to work out why a job failed.\",\n inputSchema: { name: z.string().describe(\"The job name\"), runs: z.number().int().min(1).max(100).optional().describe(\"How many recent runs to include (default 10)\") },\n },\n async ({ name, runs }) => {\n try {\n const data = await api.call<{ job: JobSummary; runs: Run[] }>(\"GET\", `/jobs/${encodeURIComponent(name)}?runs=${runs ?? 10}`);\n const def = JSON.stringify(data.job.definition, null, 2);\n return text([summarizeJob(data.job), \"\", `definition: ${def}`, \"\", `recent runs (${data.runs.length}):`, ...data.runs.map((r) => summarizeRun(r))].join(\"\\n\"));\n } catch (e) {\n return failure(e);\n }\n },\n );\n\n server.registerTool(\n \"run_check\",\n {\n title: \"Run check now\",\n description: \"Look for missed and stuck runs across every job right now and send any alerts that are due. Returns what it found.\",\n inputSchema: {},\n },\n async () => {\n try {\n const result = await api.call<{ checkedAt: number; jobs: JobSummary[]; alerts: { type: string; job: string; title: string }[]; pruned: number }>(\"POST\", \"/check\");\n const lines = [`Checked ${result.jobs.length} jobs at ${iso(result.checkedAt)}.`];\n if (result.alerts.length) lines.push(`Alerts sent: ${result.alerts.map((a) => `${a.job} ${a.type}`).join(\", \")}`);\n else lines.push(\"No new alerts.\");\n const bad = result.jobs.filter((j) => j.health !== \"healthy\");\n if (bad.length) lines.push(\"\", ...bad.map(summarizeJob));\n return text(lines.join(\"\\n\"));\n } catch (e) {\n return failure(e);\n }\n },\n );\n\n server.registerTool(\n \"silence_job\",\n {\n title: \"Silence a job\",\n description: \"Stop alerts for a job for a while, for example during a migration. Runs keep being recorded.\",\n inputSchema: { name: z.string(), for: z.string().default(\"1h\").describe('A duration like \"30m\", \"2h\", \"1d\"') },\n },\n async ({ name, for: duration }) => {\n try {\n const { state } = await api.call<{ state: { silencedUntil: number } }>(\"POST\", `/jobs/${encodeURIComponent(name)}/silence`, { for: duration });\n return text(`${name} is silenced until ${iso(state.silencedUntil)}.`);\n } catch (e) {\n return failure(e);\n }\n },\n );\n\n server.registerTool(\n \"unsilence_job\",\n { title: \"Unsilence a job\", description: \"Resume alerts for a silenced job.\", inputSchema: { name: z.string() } },\n async ({ name }) => {\n try {\n await api.call(\"POST\", `/jobs/${encodeURIComponent(name)}/unsilence`);\n return text(`${name} alerts are back on.`);\n } catch (e) {\n return failure(e);\n }\n },\n );\n\n server.registerTool(\n \"forget_job\",\n {\n title: \"Forget a job\",\n description: \"Remove a job and its run history from the store. For jobs that no longer exist in the code. A job still declared in code comes back on its next run.\",\n inputSchema: { name: z.string() },\n },\n async ({ name }) => {\n try {\n await api.call(\"DELETE\", `/jobs/${encodeURIComponent(name)}`);\n return text(`${name} removed.`);\n } catch (e) {\n return failure(e);\n }\n },\n );\n\n server.registerTool(\n \"get_setup_guide\",\n {\n title: \"How to add a job\",\n description: \"The code to add CronWatch monitoring to a scheduled job in this app: declaring the job, wrapping a route handler or function, mounting the dashboard, and running the missed-run check. Read it before writing any CronWatch code.\",\n inputSchema: {},\n },\n async () => text(SETUP_GUIDE),\n );\n\n return server;\n}\n\nexport { SETUP_GUIDE };\n","/** A small HTTP client for the JSON API that @cronwatch/sdk mounts. */\nexport interface ApiClientOptions {\n /** Where the routes are mounted, e.g. https://app.example.com/cronwatch */\n baseUrl: string;\n token: string | null;\n fetch?: typeof fetch;\n}\n\nexport class ApiError extends Error {\n constructor(message: string, readonly status: number) {\n super(message);\n this.name = \"ApiError\";\n }\n}\n\nexport class ApiClient {\n private readonly base: string;\n private readonly token: string | null;\n private readonly fetchFn: typeof fetch;\n\n constructor(options: ApiClientOptions) {\n this.base = options.baseUrl.replace(/\\/+$/, \"\");\n this.token = options.token;\n this.fetchFn = options.fetch ?? fetch;\n }\n\n async call<T = unknown>(method: string, path: string, body?: unknown): Promise<T> {\n const headers: Record<string, string> = { accept: \"application/json\" };\n if (this.token) headers.authorization = `Bearer ${this.token}`;\n if (body !== undefined) headers[\"content-type\"] = \"application/json\";\n const response = await this.fetchFn(`${this.base}/api${path}`, {\n method,\n headers,\n body: body === undefined ? undefined : JSON.stringify(body),\n });\n const text = await response.text();\n let data: unknown = null;\n try {\n data = text ? JSON.parse(text) : null;\n } catch {\n throw new ApiError(`${method} ${path} answered ${response.status} with a non-JSON body: ${text.slice(0, 200)}`, response.status);\n }\n if (!response.ok) {\n const message = typeof data === \"object\" && data && \"error\" in data ? String((data as { error: unknown }).error) : response.statusText;\n throw new ApiError(`${method} ${path} failed (${response.status}): ${message}`, response.status);\n }\n return data as T;\n }\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@cronwatch/mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for CronWatch. Lets Claude Code, Cursor and other agents list jobs, read failures, run a check and silence alerts in an app that uses @cronwatch/sdk.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/phillips-jon/cronwatch.git",
9
+ "directory": "packages/mcp"
10
+ },
11
+ "homepage": "https://cronwatch.dev/docs/mcp",
12
+ "keywords": ["mcp", "model context protocol", "cron", "monitoring", "agents", "claude"],
13
+ "type": "module",
14
+ "engines": {
15
+ "node": ">=20"
16
+ },
17
+ "bin": {
18
+ "cronwatch-mcp": "./dist/cli.js"
19
+ },
20
+ "files": ["dist", "README.md"],
21
+ "scripts": {
22
+ "build": "tsup",
23
+ "typecheck": "tsc --noEmit -p tsconfig.json",
24
+ "test": "node --import tsx --test test/*.test.ts",
25
+ "prepublishOnly": "npm run typecheck && npm test && npm run build"
26
+ },
27
+ "dependencies": {
28
+ "@modelcontextprotocol/sdk": "^1.30.0",
29
+ "zod": "^3.25.0"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "^20.19.0",
33
+ "tsup": "^8.5.1",
34
+ "tsx": "^4.23.0",
35
+ "typescript": "^5.9.3"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public"
39
+ }
40
+ }