@mrfentmen/cron-mcp 1.0.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 +21 -0
- package/dist/api.js +71 -0
- package/dist/index.js +4 -0
- package/dist/server.js +38 -0
- package/package.json +45 -0
- package/server.json +20 -0
package/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Cron MCP
|
|
2
|
+
|
|
3
|
+
Parse, describe, and compute next runs for cron expressions on the local machine. No network, no key.
|
|
4
|
+
|
|
5
|
+
This file is self contained. It reads public data only and never writes to the machine. All output is bounded and honest about what could not be fetched.
|
|
6
|
+
|
|
7
|
+
## Tools
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
* `describe` Describe a cron expression.
|
|
11
|
+
* `next` Next run times.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install
|
|
17
|
+
npm run build
|
|
18
|
+
node dist/index.js
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Parsing uses the open source cron-parser library and runs entirely locally.
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import parser from "cron-parser";
|
|
2
|
+
const parseExpression = parser.parseExpression.bind(parser);
|
|
3
|
+
export class CronError extends Error {
|
|
4
|
+
}
|
|
5
|
+
function parts(expression) {
|
|
6
|
+
const p = expression.trim().split(/\s+/);
|
|
7
|
+
if (p.length !== 5)
|
|
8
|
+
throw new CronError("Provide a 5 field cron expression like */5 * * * *");
|
|
9
|
+
return p;
|
|
10
|
+
}
|
|
11
|
+
function describeField(field, what, max, named) {
|
|
12
|
+
if (field === "*")
|
|
13
|
+
return `every ${what}`;
|
|
14
|
+
if (field.startsWith("*/"))
|
|
15
|
+
return `every ${field.slice(2)} ${what}s`;
|
|
16
|
+
if (field.includes("/")) {
|
|
17
|
+
const [base, step] = field.split("/");
|
|
18
|
+
const b = base === "*" ? "" : ` from ${base}`;
|
|
19
|
+
return `every ${step} ${what}s${b}`;
|
|
20
|
+
}
|
|
21
|
+
if (field.includes("-")) {
|
|
22
|
+
const [a, b] = field.split("-");
|
|
23
|
+
return `${what}s ${a} through ${b}`;
|
|
24
|
+
}
|
|
25
|
+
if (field.includes(",")) {
|
|
26
|
+
const vals = field.split(",").map((v) => named[v] ?? v);
|
|
27
|
+
return `${what}s ${vals.join(", ")}`;
|
|
28
|
+
}
|
|
29
|
+
return `${what} ${named[field] ?? field}`;
|
|
30
|
+
}
|
|
31
|
+
const DAYS = { "0": "Sunday", "1": "Monday", "2": "Tuesday", "3": "Wednesday", "4": "Thursday", "5": "Friday", "6": "Saturday", "7": "Sunday" };
|
|
32
|
+
const MONTHS = { "1": "January", "2": "February", "3": "March", "4": "April", "5": "May", "6": "June", "7": "July", "8": "August", "9": "September", "10": "October", "11": "November", "12": "December" };
|
|
33
|
+
export async function describe(args) {
|
|
34
|
+
const expr = (args.expression ?? "").trim();
|
|
35
|
+
if (!expr)
|
|
36
|
+
throw new CronError("Provide a cron expression");
|
|
37
|
+
const [min, hour, dom, mon, dow] = parts(expr);
|
|
38
|
+
try {
|
|
39
|
+
parseExpression(expr);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
throw new CronError("That cron expression is not valid");
|
|
43
|
+
}
|
|
44
|
+
const bits = [
|
|
45
|
+
describeField(min, "minute", 59, {}),
|
|
46
|
+
describeField(hour, "hour", 23, {}),
|
|
47
|
+
describeField(dom, "day of month", 31, {}),
|
|
48
|
+
describeField(mon, "month", 12, MONTHS),
|
|
49
|
+
describeField(dow, "day of week", 7, DAYS),
|
|
50
|
+
];
|
|
51
|
+
return `Cron \"${expr}\" runs:\n ${bits.join(",\n ")}`;
|
|
52
|
+
}
|
|
53
|
+
export async function next(args) {
|
|
54
|
+
const expr = (args.expression ?? "").trim();
|
|
55
|
+
if (!expr)
|
|
56
|
+
throw new CronError("Provide a cron expression");
|
|
57
|
+
const count = Math.min(args.count ?? 5, 20);
|
|
58
|
+
let cron;
|
|
59
|
+
try {
|
|
60
|
+
cron = parseExpression(expr);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
throw new CronError("That cron expression is not valid");
|
|
64
|
+
}
|
|
65
|
+
const rows = [];
|
|
66
|
+
for (let i = 0; i < count; i++) {
|
|
67
|
+
const d = cron.next();
|
|
68
|
+
rows.push(`${i + 1}. ${d.toISOString().replace("T", " ").slice(0, 16)}`);
|
|
69
|
+
}
|
|
70
|
+
return `Next ${count} runs of \"${expr}\":\n` + rows.join("\n");
|
|
71
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2
|
+
import { createServer } from "./server.js";
|
|
3
|
+
const main = async () => { const server = createServer(); await server.connect(new StdioServerTransport()); };
|
|
4
|
+
main().catch((error) => { console.error("Fatal error:", error); process.exit(1); });
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { describe } from "./api.js";
|
|
4
|
+
import { next } from "./api.js";
|
|
5
|
+
const text = (value) => ({ content: [{ type: "text", text: value }] });
|
|
6
|
+
const textError = (t) => ({ content: [{ type: "text", text: t }], isError: true });
|
|
7
|
+
const READ_ONLY = { readOnlyHint: true, openWorldHint: true };
|
|
8
|
+
const error = (e) => `Error: ${e instanceof Error ? e.message : String(e)}`;
|
|
9
|
+
export function createServer() {
|
|
10
|
+
const server = new McpServer({ name: "cron-mcp", version: "1.0.0" });
|
|
11
|
+
server.registerTool("describe", {
|
|
12
|
+
title: "Describe",
|
|
13
|
+
description: "Describe a cron expression in words.",
|
|
14
|
+
inputSchema: z.object({ expression: z.string().describe("5 field cron expression.") }),
|
|
15
|
+
annotations: READ_ONLY,
|
|
16
|
+
}, async (args) => {
|
|
17
|
+
try {
|
|
18
|
+
return text(await describe(args));
|
|
19
|
+
}
|
|
20
|
+
catch (e) {
|
|
21
|
+
return textError(error(e));
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
server.registerTool("next", {
|
|
25
|
+
title: "Next",
|
|
26
|
+
description: "Next run times for a cron expression.",
|
|
27
|
+
inputSchema: z.object({ expression: z.string().describe("5 field cron expression."), count: z.number().describe("Number of runs.").optional() }),
|
|
28
|
+
annotations: READ_ONLY,
|
|
29
|
+
}, async (args) => {
|
|
30
|
+
try {
|
|
31
|
+
return text(await next(args));
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
return textError(error(e));
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
return server;
|
|
38
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.0.0",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "https://github.com/mrfentmen/cron-mcp.git"
|
|
7
|
+
},
|
|
8
|
+
"bin": {
|
|
9
|
+
"cron-mcp": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"main": "./dist/index.js",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"server.json",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc -p tsconfig.json",
|
|
19
|
+
"start": "node dist/index.js",
|
|
20
|
+
"dev": "npm run build && node dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
25
|
+
"zod": "^3.23.8",
|
|
26
|
+
"cron-parser": "^4.9.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^22.0.0",
|
|
30
|
+
"typescript": "^5.6.0"
|
|
31
|
+
},
|
|
32
|
+
"name": "@mrfentmen/cron-mcp",
|
|
33
|
+
"description": "Parse, describe, and compute next runs for cron expressions locally. No network and no key.",
|
|
34
|
+
"mcpName": "io.github.mrfentmen/cron-mcp",
|
|
35
|
+
"keywords": [
|
|
36
|
+
"mcp",
|
|
37
|
+
"cron",
|
|
38
|
+
"schedule",
|
|
39
|
+
"scheduler",
|
|
40
|
+
"devops"
|
|
41
|
+
],
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=20"
|
|
44
|
+
}
|
|
45
|
+
}
|
package/server.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.mrfentmen/cron-mcp",
|
|
4
|
+
"description": "Parse, describe, and compute next runs for cron expressions locally. No network and no key.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"url": "https://github.com/mrfentmen/cron-mcp",
|
|
7
|
+
"source": "github"
|
|
8
|
+
},
|
|
9
|
+
"version": "1.0.0",
|
|
10
|
+
"packages": [
|
|
11
|
+
{
|
|
12
|
+
"registryType": "npm",
|
|
13
|
+
"identifier": "@mrfentmen/cron-mcp",
|
|
14
|
+
"version": "1.0.0",
|
|
15
|
+
"transport": {
|
|
16
|
+
"type": "stdio"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|