queue-inspector-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/CHANGELOG.md +24 -0
- package/LICENSE +21 -0
- package/README.md +148 -0
- package/dist/backends/asynq-proto.d.ts +16 -0
- package/dist/backends/asynq-proto.js +144 -0
- package/dist/backends/asynq-proto.js.map +1 -0
- package/dist/backends/asynq.d.ts +45 -0
- package/dist/backends/asynq.js +183 -0
- package/dist/backends/asynq.js.map +1 -0
- package/dist/backends/bullmq.d.ts +46 -0
- package/dist/backends/bullmq.js +279 -0
- package/dist/backends/bullmq.js.map +1 -0
- package/dist/backends/index.d.ts +19 -0
- package/dist/backends/index.js +57 -0
- package/dist/backends/index.js.map +1 -0
- package/dist/backends/lua/deleteTask.lua +41 -0
- package/dist/backends/lua/removeJob.lua +346 -0
- package/dist/backends/lua/reprocessJob.lua +113 -0
- package/dist/backends/lua/runTask.lua +39 -0
- package/dist/backends/lua.d.ts +18 -0
- package/dist/backends/lua.js +18 -0
- package/dist/backends/lua.js.map +1 -0
- package/dist/backends/scripting.d.ts +14 -0
- package/dist/backends/scripting.js +34 -0
- package/dist/backends/scripting.js.map +1 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.js +32 -0
- package/dist/config.js.map +1 -0
- package/dist/format.d.ts +16 -0
- package/dist/format.js +49 -0
- package/dist/format.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/redis.d.ts +13 -0
- package/dist/redis.js +30 -0
- package/dist/redis.js.map +1 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.js +105 -0
- package/dist/server.js.map +1 -0
- package/dist/types.d.ts +64 -0
- package/dist/types.js +14 -0
- package/dist/types.js.map +1 -0
- package/package.json +56 -0
package/dist/format.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const utf8Fatal = new TextDecoder("utf-8", { fatal: true });
|
|
2
|
+
/** True when the buffer decodes as UTF-8 and holds no C0 control bytes other
|
|
3
|
+
* than tab, newline and carriage return. Anything else is treated as binary. */
|
|
4
|
+
function looksLikeText(buf) {
|
|
5
|
+
try {
|
|
6
|
+
utf8Fatal.decode(buf);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
for (const byte of buf) {
|
|
12
|
+
if (byte < 0x20 && byte !== 0x09 && byte !== 0x0a && byte !== 0x0d)
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Renders a raw payload for display. Text is returned as UTF-8; binary or
|
|
19
|
+
* otherwise non-text bytes are returned base64-encoded. Either way the result
|
|
20
|
+
* is capped at `maxBytes` of source data so a large payload cannot flood a tool
|
|
21
|
+
* response.
|
|
22
|
+
*/
|
|
23
|
+
export function decodePayload(buf, maxBytes = 8 * 1024) {
|
|
24
|
+
const truncated = buf.length > maxBytes;
|
|
25
|
+
const slice = truncated ? buf.subarray(0, maxBytes) : buf;
|
|
26
|
+
const isText = looksLikeText(slice);
|
|
27
|
+
return {
|
|
28
|
+
payload: isText ? slice.toString("utf8") : slice.toString("base64"),
|
|
29
|
+
payloadEncoding: isText ? "utf8" : "base64",
|
|
30
|
+
payloadBytes: buf.length,
|
|
31
|
+
payloadTruncated: truncated,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function truncate(text, max = 240) {
|
|
35
|
+
if (text.length <= max)
|
|
36
|
+
return text;
|
|
37
|
+
return `${text.slice(0, max)}… (${text.length} chars)`;
|
|
38
|
+
}
|
|
39
|
+
export function isoFromUnixSeconds(seconds) {
|
|
40
|
+
if (!seconds || seconds <= 0)
|
|
41
|
+
return null;
|
|
42
|
+
return new Date(seconds * 1000).toISOString();
|
|
43
|
+
}
|
|
44
|
+
export function isoFromMillis(ms) {
|
|
45
|
+
if (!ms || ms <= 0)
|
|
46
|
+
return null;
|
|
47
|
+
return new Date(ms).toISOString();
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=format.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.js","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"AAAA,MAAM,SAAS,GAAG,IAAI,WAAW,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AAS5D;iFACiF;AACjF,SAAS,aAAa,CAAC,GAAW;IAChC,IAAI,CAAC;QACH,SAAS,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACxB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,GAAG,EAAE,CAAC;QACvB,IAAI,IAAI,GAAG,IAAI,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,IAAI;YAAE,OAAO,KAAK,CAAC;IACnF,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,GAAW,EAAE,QAAQ,GAAG,CAAC,GAAG,IAAI;IAC5D,MAAM,SAAS,GAAG,GAAG,CAAC,MAAM,GAAG,QAAQ,CAAC;IACxC,MAAM,KAAK,GAAG,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;IAC1D,MAAM,MAAM,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;IACpC,OAAO;QACL,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC;QACnE,eAAe,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ;QAC3C,YAAY,EAAE,GAAG,CAAC,MAAM;QACxB,gBAAgB,EAAE,SAAS;KAC5B,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,IAAY,EAAE,GAAG,GAAG,GAAG;IAC9C,IAAI,IAAI,CAAC,MAAM,IAAI,GAAG;QAAE,OAAO,IAAI,CAAC;IACpC,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,MAAM,IAAI,CAAC,MAAM,SAAS,CAAC;AACzD,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,OAAkC;IACnE,IAAI,CAAC,OAAO,IAAI,OAAO,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1C,OAAO,IAAI,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;AAChD,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,EAA6B;IACzD,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAChC,OAAO,IAAI,IAAI,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;AACpC,CAAC"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { loadConfig } from "./config.js";
|
|
4
|
+
import { createRedis } from "./redis.js";
|
|
5
|
+
import { BackendRegistry } from "./backends/index.js";
|
|
6
|
+
import { createServer } from "./server.js";
|
|
7
|
+
async function main() {
|
|
8
|
+
const config = loadConfig();
|
|
9
|
+
const redis = createRedis(config.redisUrl);
|
|
10
|
+
const registry = new BackendRegistry(redis, config);
|
|
11
|
+
const server = createServer({ registry, readOnly: config.readOnly });
|
|
12
|
+
const shutdown = async () => {
|
|
13
|
+
try {
|
|
14
|
+
await server.close();
|
|
15
|
+
}
|
|
16
|
+
finally {
|
|
17
|
+
redis.disconnect();
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
process.on("SIGINT", () => void shutdown().then(() => process.exit(0)));
|
|
21
|
+
process.on("SIGTERM", () => void shutdown().then(() => process.exit(0)));
|
|
22
|
+
const transport = new StdioServerTransport();
|
|
23
|
+
await server.connect(transport);
|
|
24
|
+
// stderr is safe for logs; stdout carries the MCP protocol.
|
|
25
|
+
const mode = config.readOnly ? " (read-only)" : "";
|
|
26
|
+
process.stderr.write(`queue-inspector-mcp: connected, backends=[${config.backends.join(", ")}]${mode}\n`);
|
|
27
|
+
}
|
|
28
|
+
main().catch((err) => {
|
|
29
|
+
process.stderr.write(`queue-inspector-mcp: fatal: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
});
|
|
32
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,MAAM,KAAK,GAAG,WAAW,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC3C,MAAM,QAAQ,GAAG,IAAI,eAAe,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IACpD,MAAM,MAAM,GAAG,YAAY,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;IAErE,MAAM,QAAQ,GAAG,KAAK,IAAI,EAAE;QAC1B,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;QACvB,CAAC;gBAAS,CAAC;YACT,KAAK,CAAC,UAAU,EAAE,CAAC;QACrB,CAAC;IACH,CAAC,CAAC;IACF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,KAAK,QAAQ,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACxE,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,KAAK,QAAQ,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAEzE,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAChC,4DAA4D;IAC5D,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,CAAC;IACnD,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,6CAA6C,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,CACpF,CAAC;AACJ,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;IAC5B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,+BAA+B,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACvH,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
package/dist/redis.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Redis } from "ioredis";
|
|
2
|
+
/**
|
|
3
|
+
* Creates an ioredis client. `lazyConnect` keeps startup cheap: the connection
|
|
4
|
+
* opens on first command, so the process can register tools before Redis is
|
|
5
|
+
* reachable. `SCAN`-based discovery below never uses `KEYS`, so a large
|
|
6
|
+
* production database is walked in bounded batches.
|
|
7
|
+
*/
|
|
8
|
+
export declare function createRedis(url: string): Redis;
|
|
9
|
+
/**
|
|
10
|
+
* Iterates keys matching a pattern with `SCAN`, collecting the results. Uses a
|
|
11
|
+
* generous `COUNT` hint but never blocks Redis the way `KEYS` would.
|
|
12
|
+
*/
|
|
13
|
+
export declare function scanKeys(redis: Redis, match: string, count?: number): Promise<string[]>;
|
package/dist/redis.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Redis } from "ioredis";
|
|
2
|
+
/**
|
|
3
|
+
* Creates an ioredis client. `lazyConnect` keeps startup cheap: the connection
|
|
4
|
+
* opens on first command, so the process can register tools before Redis is
|
|
5
|
+
* reachable. `SCAN`-based discovery below never uses `KEYS`, so a large
|
|
6
|
+
* production database is walked in bounded batches.
|
|
7
|
+
*/
|
|
8
|
+
export function createRedis(url) {
|
|
9
|
+
return new Redis(url, {
|
|
10
|
+
lazyConnect: true,
|
|
11
|
+
maxRetriesPerRequest: 3,
|
|
12
|
+
enableReadyCheck: true,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Iterates keys matching a pattern with `SCAN`, collecting the results. Uses a
|
|
17
|
+
* generous `COUNT` hint but never blocks Redis the way `KEYS` would.
|
|
18
|
+
*/
|
|
19
|
+
export async function scanKeys(redis, match, count = 500) {
|
|
20
|
+
const found = [];
|
|
21
|
+
let cursor = "0";
|
|
22
|
+
do {
|
|
23
|
+
const [next, batch] = await redis.scan(cursor, "MATCH", match, "COUNT", count);
|
|
24
|
+
cursor = next;
|
|
25
|
+
for (const key of batch)
|
|
26
|
+
found.push(key);
|
|
27
|
+
} while (cursor !== "0");
|
|
28
|
+
return found;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=redis.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis.js","sourceRoot":"","sources":["../src/redis.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEhC;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,OAAO,IAAI,KAAK,CAAC,GAAG,EAAE;QACpB,WAAW,EAAE,IAAI;QACjB,oBAAoB,EAAE,CAAC;QACvB,gBAAgB,EAAE,IAAI;KACvB,CAAC,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,KAAY,EAAE,KAAa,EAAE,KAAK,GAAG,GAAG;IACrE,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,MAAM,GAAG,GAAG,CAAC;IACjB,GAAG,CAAC;QACF,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;QAC/E,MAAM,GAAG,IAAI,CAAC;QACd,KAAK,MAAM,GAAG,IAAI,KAAK;YAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC3C,CAAC,QAAQ,MAAM,KAAK,GAAG,EAAE;IACzB,OAAO,KAAK,CAAC;AACf,CAAC"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { BackendRegistry } from "./backends/index.js";
|
|
3
|
+
export interface ServerOptions {
|
|
4
|
+
registry: BackendRegistry;
|
|
5
|
+
readOnly: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function createServer({ registry, readOnly }: ServerOptions): McpServer;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { BackendError } from "./types.js";
|
|
4
|
+
const SERVER_VERSION = "0.1.0";
|
|
5
|
+
const backendArg = z
|
|
6
|
+
.enum(["asynq", "bullmq"])
|
|
7
|
+
.optional()
|
|
8
|
+
.describe("Which backend owns the queue. Optional when the queue name is unique across backends.");
|
|
9
|
+
const queueArg = z.string().min(1).describe("Queue name, as reported by list_queues.");
|
|
10
|
+
const idArg = z.string().min(1).describe("Job or task id.");
|
|
11
|
+
function ok(data) {
|
|
12
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
13
|
+
}
|
|
14
|
+
function fail(message) {
|
|
15
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
16
|
+
}
|
|
17
|
+
/** Runs a handler, turning known backend errors and connection failures into
|
|
18
|
+
* tool errors instead of letting them crash the transport. */
|
|
19
|
+
async function guard(run) {
|
|
20
|
+
try {
|
|
21
|
+
return await run();
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
if (err instanceof BackendError)
|
|
25
|
+
return fail(`${err.code}: ${err.message}`);
|
|
26
|
+
return fail(err instanceof Error ? err.message : String(err));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function createServer({ registry, readOnly }) {
|
|
30
|
+
const server = new McpServer({ name: "queue-inspector-mcp", version: SERVER_VERSION });
|
|
31
|
+
server.registerTool("list_queues", {
|
|
32
|
+
title: "List queues",
|
|
33
|
+
description: "List every queue the inspector can see, tagged with its backend (asynq or bullmq).",
|
|
34
|
+
inputSchema: { backend: backendArg },
|
|
35
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
36
|
+
}, async ({ backend }) => guard(async () => {
|
|
37
|
+
const queues = await registry.allQueues();
|
|
38
|
+
const filtered = backend ? queues.filter((q) => q.backend === backend) : queues;
|
|
39
|
+
return ok({ count: filtered.length, queues: filtered });
|
|
40
|
+
}));
|
|
41
|
+
server.registerTool("queue_stats", {
|
|
42
|
+
title: "Queue stats",
|
|
43
|
+
description: "Report the number of jobs in each state for a queue, using the backend's own state names.",
|
|
44
|
+
inputSchema: { queue: queueArg, backend: backendArg },
|
|
45
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
46
|
+
}, async ({ queue, backend }) => guard(async () => {
|
|
47
|
+
const b = await registry.resolve(queue, backend);
|
|
48
|
+
return ok({ queue, backend: b.name, states: await b.stats(queue) });
|
|
49
|
+
}));
|
|
50
|
+
server.registerTool("list_jobs", {
|
|
51
|
+
title: "List jobs",
|
|
52
|
+
description: "List jobs in a given state (paged). Returns id, type, attempts and a truncated last error. " +
|
|
53
|
+
"Valid states depend on the backend: asynq uses pending/active/scheduled/retry/archived/completed; " +
|
|
54
|
+
"bullmq uses waiting/active/delayed/prioritized/waiting-children/paused/completed/failed.",
|
|
55
|
+
inputSchema: {
|
|
56
|
+
queue: queueArg,
|
|
57
|
+
state: z.string().min(1).describe("State to list, e.g. \"failed\" (bullmq) or \"archived\" (asynq)."),
|
|
58
|
+
backend: backendArg,
|
|
59
|
+
offset: z.number().int().min(0).default(0).describe("Number of jobs to skip."),
|
|
60
|
+
limit: z.number().int().min(1).max(200).default(20).describe("Maximum jobs to return."),
|
|
61
|
+
},
|
|
62
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
63
|
+
}, async ({ queue, state, backend, offset, limit }) => guard(async () => {
|
|
64
|
+
const b = await registry.resolve(queue, backend);
|
|
65
|
+
const jobs = await b.listJobs(queue, state, { offset, limit });
|
|
66
|
+
return ok({ queue, backend: b.name, state, offset, limit, count: jobs.length, jobs });
|
|
67
|
+
}));
|
|
68
|
+
server.registerTool("get_job", {
|
|
69
|
+
title: "Get job",
|
|
70
|
+
description: "Fetch full detail for one job: payload, attempts, retry ceiling, last error and timestamps. " +
|
|
71
|
+
"Binary payloads are returned base64-encoded and flagged.",
|
|
72
|
+
inputSchema: { queue: queueArg, id: idArg, backend: backendArg },
|
|
73
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
74
|
+
}, async ({ queue, id, backend }) => guard(async () => {
|
|
75
|
+
const b = await registry.resolve(queue, backend);
|
|
76
|
+
const job = await b.getJob(queue, id);
|
|
77
|
+
if (!job)
|
|
78
|
+
return fail(`job_not_found: no job "${id}" in queue "${queue}"`);
|
|
79
|
+
return ok(job);
|
|
80
|
+
}));
|
|
81
|
+
if (!readOnly) {
|
|
82
|
+
server.registerTool("retry_job", {
|
|
83
|
+
title: "Retry job",
|
|
84
|
+
description: "Move a failed or dead job back to the pending/wait queue so it runs again. " +
|
|
85
|
+
"Faithfully replicates the backend's own retry (asynq Inspector.RunTask, bullmq Job.retry).",
|
|
86
|
+
inputSchema: { queue: queueArg, id: idArg, backend: backendArg },
|
|
87
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
88
|
+
}, async ({ queue, id, backend }) => guard(async () => {
|
|
89
|
+
const b = await registry.resolve(queue, backend);
|
|
90
|
+
return ok(await b.retryJob(queue, id));
|
|
91
|
+
}));
|
|
92
|
+
server.registerTool("delete_job", {
|
|
93
|
+
title: "Delete job",
|
|
94
|
+
description: "Permanently delete a job from a queue. Active jobs cannot be deleted. " +
|
|
95
|
+
"Faithfully replicates the backend's own delete (asynq Inspector.DeleteTask, bullmq Job.remove).",
|
|
96
|
+
inputSchema: { queue: queueArg, id: idArg, backend: backendArg },
|
|
97
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
98
|
+
}, async ({ queue, id, backend }) => guard(async () => {
|
|
99
|
+
const b = await registry.resolve(queue, backend);
|
|
100
|
+
return ok(await b.deleteJob(queue, id));
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
return server;
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export type BackendName = "asynq" | "bullmq";
|
|
2
|
+
export interface QueueRef {
|
|
3
|
+
name: string;
|
|
4
|
+
backend: BackendName;
|
|
5
|
+
}
|
|
6
|
+
/** Counts keyed by each backend's own state names. */
|
|
7
|
+
export type StateCounts = Record<string, number>;
|
|
8
|
+
export interface JobSummary {
|
|
9
|
+
id: string;
|
|
10
|
+
/** Asynq task type or BullMQ job name. */
|
|
11
|
+
type: string;
|
|
12
|
+
/** The backend's own state name (e.g. "retry", "archived", "failed"). */
|
|
13
|
+
state: string;
|
|
14
|
+
/** ISO 8601, or null when the backend does not record an enqueue time. */
|
|
15
|
+
enqueuedAt: string | null;
|
|
16
|
+
/** Retries performed so far (Asynq `retried`, BullMQ `attemptsMade`). */
|
|
17
|
+
attempts: number | null;
|
|
18
|
+
/** Configured retry ceiling (Asynq `retry`, BullMQ `opts.attempts`). */
|
|
19
|
+
maxRetries: number | null;
|
|
20
|
+
/** Last failure message, truncated for list views. */
|
|
21
|
+
lastError: string | null;
|
|
22
|
+
}
|
|
23
|
+
export interface JobDetail extends JobSummary {
|
|
24
|
+
queue: string;
|
|
25
|
+
backend: BackendName;
|
|
26
|
+
/** Payload decoded as UTF-8 text, or base64 when the bytes are not valid text. */
|
|
27
|
+
payload: string;
|
|
28
|
+
payloadEncoding: "utf8" | "base64";
|
|
29
|
+
/** Size of the original payload in bytes, before any truncation. */
|
|
30
|
+
payloadBytes: number;
|
|
31
|
+
payloadTruncated: boolean;
|
|
32
|
+
/** Backend-specific timestamps (ISO 8601), null when not set. */
|
|
33
|
+
timestamps: Record<string, string | null>;
|
|
34
|
+
/** Full, untruncated last failure message. */
|
|
35
|
+
fullError: string | null;
|
|
36
|
+
}
|
|
37
|
+
export interface PageOpts {
|
|
38
|
+
offset: number;
|
|
39
|
+
limit: number;
|
|
40
|
+
}
|
|
41
|
+
export interface MutationResult {
|
|
42
|
+
ok: true;
|
|
43
|
+
message: string;
|
|
44
|
+
}
|
|
45
|
+
export interface QueueBackend {
|
|
46
|
+
readonly name: BackendName;
|
|
47
|
+
/** State names this backend reports, in a sensible display order. */
|
|
48
|
+
readonly states: readonly string[];
|
|
49
|
+
detectQueues(): Promise<string[]>;
|
|
50
|
+
stats(queue: string): Promise<StateCounts>;
|
|
51
|
+
listJobs(queue: string, state: string, page: PageOpts): Promise<JobSummary[]>;
|
|
52
|
+
getJob(queue: string, id: string): Promise<JobDetail | null>;
|
|
53
|
+
retryJob(queue: string, id: string): Promise<MutationResult>;
|
|
54
|
+
deleteJob(queue: string, id: string): Promise<MutationResult>;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Raised for conditions a caller can act on: an unknown queue or job, or a job
|
|
58
|
+
* whose current state does not allow the requested operation. The server turns
|
|
59
|
+
* these into tool errors rather than crashing.
|
|
60
|
+
*/
|
|
61
|
+
export declare class BackendError extends Error {
|
|
62
|
+
readonly code: "queue_not_found" | "job_not_found" | "invalid_state" | "not_allowed";
|
|
63
|
+
constructor(message: string, code: "queue_not_found" | "job_not_found" | "invalid_state" | "not_allowed");
|
|
64
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raised for conditions a caller can act on: an unknown queue or job, or a job
|
|
3
|
+
* whose current state does not allow the requested operation. The server turns
|
|
4
|
+
* these into tool errors rather than crashing.
|
|
5
|
+
*/
|
|
6
|
+
export class BackendError extends Error {
|
|
7
|
+
code;
|
|
8
|
+
constructor(message, code) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.name = "BackendError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AA+DA;;;;GAIG;AACH,MAAM,OAAO,YAAa,SAAQ,KAAK;IAG1B;IAFX,YACE,OAAe,EACN,IAIQ;QAEjB,KAAK,CAAC,OAAO,CAAC,CAAC;QANN,SAAI,GAAJ,IAAI,CAII;QAGjB,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;IAC7B,CAAC;CACF"}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "queue-inspector-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"mcpName": "io.github.yusufihsangorgel/queue-inspector-mcp",
|
|
5
|
+
"description": "MCP server for inspecting and operating Redis-backed job queues (Asynq and BullMQ).",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"queue-inspector-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "dist/index.js",
|
|
11
|
+
"types": "dist/index.d.ts",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE",
|
|
16
|
+
"CHANGELOG.md"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc && node scripts/copy-assets.mjs",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"prepublishOnly": "npm run build"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"mcp",
|
|
29
|
+
"modelcontextprotocol",
|
|
30
|
+
"redis",
|
|
31
|
+
"queue",
|
|
32
|
+
"bullmq",
|
|
33
|
+
"asynq",
|
|
34
|
+
"jobs"
|
|
35
|
+
],
|
|
36
|
+
"author": "Yusuf İhsan Görgel",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/Yusufihsangorgel/queue-inspector-mcp"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/Yusufihsangorgel/queue-inspector-mcp#readme",
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/Yusufihsangorgel/queue-inspector-mcp/issues"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@modelcontextprotocol/sdk": "^1.18.0",
|
|
48
|
+
"ioredis": "^5.4.1",
|
|
49
|
+
"zod": "^3.23.8"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^22.10.0",
|
|
53
|
+
"typescript": "^5.7.2",
|
|
54
|
+
"vitest": "^2.1.8"
|
|
55
|
+
}
|
|
56
|
+
}
|