npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

multi-tasks-mcp

v0.0.1

Published

MCP server that drives multi-tasks parallel batch jobs for AI agents

Readme

multi-tasks-mcp

MCP server that lets AI agents drive multi-tasks parallel batch jobs: multi-process execution, crash resume, failed-task retry.

Security: task logic is arbitrary JavaScript executed on this host with the current user's permissions. Connecting this server to an MCP client is equivalent to allowing that AI to run any code on your machine. Only connect trusted MCP clients.

Install & configure

npm i multi-tasks-mcp

MCP host config example (stdio):

{
  "mcpServers": {
    "multi-tasks": {
      "command": "npx",
      "args": ["-y", "multi-tasks-mcp"],
      "env": { "MULTI_TASKS_MCP_JOB_ROOT": "/abs/path/to/jobs" }
    }
  }
}

MULTI_TASKS_MCP_JOB_ROOT is optional; it defaults to <cwd>/.jobs. All job directories are created under this root.

Task module contract

A task module is a CommonJS module:

module.exports = {
  initialTasks,   // optional: array or () => array | Promise<array>
  processTask,    // required: (task, helper) => result | Promise<result>
  options,        // optional: { numberOfWorkers, taskTimeout, maxTaskRetries, shouldTerminate, setSysListener }
};

create_job's tasks argument takes precedence over initialTasks.

Two rules every task module must follow:

  1. Task functions must be self-contained. Any module used inside processTask / initialTasks must be required inside the function body (e.g. write const fs = require('fs'); in the body, not at module top level). Reason: on resume_job (resume / retry_fails / restart) multi-tasks revives these functions from their serialized source stored in the job's config archive (task_config.json); top-level requires are lost, and referencing outer variables then throws ReferenceError.
  2. Crashed tasks are not re-queued by default. When task code crashes a worker, the failed task is only re-executed if maxTaskRetries was set explicitly (or you later call resume_job with mode retry_fails); otherwise the task ends as failed. Set maxTaskRetries for any batch you care about.

How to operate

The server is asynchronous: create_job returns immediately and the job runs in a detached runner process. The typical operation loop is create → poll → collect, with stop/resume for long or stuck jobs:

  1. Create: call create_job with one of processTask / processTaskCode / processTaskFile, plus tasks. Keep the returned jobId — it is the handle for everything below.
  2. Poll: call job_status with the jobId every few seconds. counts shows live progress (pending / running / succeeded / failed). Stop polling when state becomes done or exited.
  3. Collect: when state is done, call job_results with kind: 'succ' for results and kind: 'errors' for failures, paging with offset/limit for large batches.
  4. Troubleshoot: when state is exited (runner crashed, was killed, or stop_job was called), read logTail from job_status for the cause, then call resume_job with mode resume to continue from where it stopped, retry_fails to also re-run failed tasks, or restart to start over.
  5. Stop early: call stop_job to kill a running job. Nothing is lost — progress stays on disk and resume_job brings it back.

End-to-end example (any MCP client; here the official SDK):

//callJson: thin wrapper around client.callTool that parses the JSON text result
const callJson = async (name, args) => {
  const res = await client.callTool({ name, arguments: args });
  if (res.isError) {
    throw new Error(res.content[0].text);
  };
  return JSON.parse(res.content[0].text);
};

const { jobId } = await callJson('create_job', {
  processTask: `(task) => {
    const fs = require('fs');//required inside the function body, see the contract above
    return { squared: task.n * task.n };
  }`,
  tasks: [{ n: 1 }, { n: 2 }, { n: 3 }],
  numberOfWorkers: 2,
  maxTaskRetries: 2,
});

//poll until the job leaves the running state
let status;
do {
  await new Promise((r) => setTimeout(r, 2000));
  status = await callJson('job_status', { jobId });
} while (status.state === 'running' || status.state === 'unknown');

if (status.state === 'exited') {
  console.error('runner exited, log tail:', status.logTail);
  await callJson('resume_job', { jobId, mode: 'resume' });//then keep polling
} else {
  const results = await callJson('job_results', { jobId, kind: 'succ' });
  console.log(results.items.map((it) => it.result));
};

Where things live on disk (per job, under MULTI_TASKS_MCP_JOB_ROOT/<jobId>/):

  • runner.log — runner process stdout/stderr; first place to look when a job misbehaves.
  • job.done.json — the final report; its presence is what makes state become done.
  • data/<jobId>/ — multi-tasks' own files: progress/ (per-task state), results/succ + results/errors (per-task results), .sys/run.log (multi-tasks' internal log).

Tools

All tools return JSON text. Errors come back with isError: true and a message.

create_job

Create and start a parallel batch job in the background. Provide task logic via exactly one of processTask, processTaskCode, or processTaskFile (mutually exclusive). Tasks come from tasks (JSON array) or the module's initialTasks; with processTask the tasks array is required.

Arguments:

  • processTask (string, optional): source text of the processTask(task, helper) function itself — what processTask.toString() returns, e.g. (task) => { return {...}; } or async (task) => {...}. The server wraps it into a module for you. Requires tasks.
  • processTaskCode (string, optional): source of a full CommonJS module exporting processTask(task, helper); optionally initialTasks and options. Use this when you need initialTasks, options, or helper functions around processTask.
  • processTaskFile (string, optional): absolute path of such a module file.
  • tasks (array, optional): task objects array; takes precedence over module initialTasks.
  • numberOfWorkers (integer, optional, 1–32).
  • taskTimeout (integer, optional): per-task timeout in ms.
  • maxTaskRetries (integer, optional, 0–10).

Call example (simplest form):

await client.callTool({
  name: 'create_job',
  arguments: {
    processTask: `(task) => { return { squared: task.n * task.n }; }`,
    tasks: [{ n: 1 }, { n: 2 }, { n: 3 }],
    numberOfWorkers: 2,
  },
});

Call example (full module form):

await client.callTool({
  name: 'create_job',
  arguments: {
    processTaskCode: `module.exports = {
      initialTasks: [{ n: 1 }, { n: 2 }, { n: 3 }],
      processTask: (task) => { return { squared: task.n * task.n }; },
      options: { numberOfWorkers: 2 },
    };`,
  },
});

Return example:

{
  "jobId": "job-20260901-080000-a1b2c3",
  "jobDir": "C:\\work\\.jobs\\job-20260901-080000-a1b2c3",
  "taskFolder": "C:\\work\\.jobs\\job-20260901-080000-a1b2c3\\data\\job-20260901-080000-a1b2c3",
  "pid": 12345
}

list_jobs

List all jobs known to this server, newest first. No arguments.

Call example:

await client.callTool({ name: 'list_jobs', arguments: {} });

Return example:

[
  { "jobId": "job-20260901-080000-a1b2c3", "state": "done", "createdAt": "2026-09-01T08:00:00.000Z" }
]

job_status

Get job state (unknown / running / exited / done), per-queue task counts, final report (when done) and runner log tail.

Arguments:

  • jobId (string, required): jobId returned by create_job.

Call example:

await client.callTool({ name: 'job_status', arguments: { jobId: 'job-20260901-080000-a1b2c3' } });

Return example (done job):

{
  "jobId": "job-20260901-080000-a1b2c3",
  "state": "done",
  "counts": { "pending": 0, "running": 0, "succeeded": 3, "failed": 0 },
  "report": {
    "taskId": "job-20260901-080000-a1b2c3",
    "taskFolder": ".jobs/job-20260901-080000-a1b2c3/data/job-20260901-080000-a1b2c3",
    "startTimestamp": 1788100000000,
    "endTimestamp": 1788100004200,
    "cost": 4200,
    "total": 3,
    "succeeded": 3,
    "failed": 0,
    "failedTasks": [],
    "succButSaveFailed": 0,
    "workers": { "count": 2, "crashed": 0 }
  },
  "logTail": "..."
}

report is null until the job finishes.

job_results

Read task results paginated. kind="succ" for succeeded results, kind="errors" for failed ones. limit is capped at 200.

Arguments:

  • jobId (string, required).
  • kind ("succ" | "errors", optional, default "succ").
  • offset (integer >= 0, optional, default 0).
  • limit (integer >= 1, optional, default 50, capped at 200).

Call example:

await client.callTool({
  name: 'job_results',
  arguments: { jobId: 'job-20260901-080000-a1b2c3', kind: 'succ' },
});

Return example:

{
  "kind": "succ",
  "total": 3,
  "offset": 0,
  "limit": 50,
  "items": [
    {
      "subid": "job-20260901-080000-a1b2c3--k3x9az",
      "task": { "n": 1, "masterTaskId": "job-20260901-080000-a1b2c3", "subid": "job-20260901-080000-a1b2c3--k3x9az" },
      "result": { "squared": 1 }
    },
    {
      "subid": "job-20260901-080000-a1b2c3--m7q2bw",
      "task": { "n": 2, "masterTaskId": "job-20260901-080000-a1b2c3", "subid": "job-20260901-080000-a1b2c3--m7q2bw" },
      "result": { "squared": 4 }
    },
    {
      "subid": "job-20260901-080000-a1b2c3--p8r4cx",
      "task": { "n": 3, "masterTaskId": "job-20260901-080000-a1b2c3", "subid": "job-20260901-080000-a1b2c3--p8r4cx" },
      "result": { "squared": 9 }
    }
  ]
}

Note: multi-tasks annotates each task with masterTaskId and subid before running it, so items[].task contains your original fields plus those two.

stop_job

Kill the runner process tree of a running job. Progress is kept on disk; use resume_job to continue later.

Arguments:

  • jobId (string, required).

Call example:

await client.callTool({ name: 'stop_job', arguments: { jobId: 'job-20260901-080000-a1b2c3' } });

Return example:

{ "jobId": "job-20260901-080000-a1b2c3", "stopped": true }

stopped is false when the runner was not alive (already finished or exited).

resume_job

Restart a stopped/exited job runner. Mode "resume" continues pending tasks; "retry_fails" also re-runs failed tasks; "restart" wipes progress and starts over. Fails if the job is currently running.

Arguments:

  • jobId (string, required).
  • mode ("resume" | "retry_fails" | "restart", required).

Call example:

await client.callTool({
  name: 'resume_job',
  arguments: { jobId: 'job-20260901-080000-a1b2c3', mode: 'resume' },
});

Return example:

{ "jobId": "job-20260901-080000-a1b2c3", "pid": 23456 }

Job states

unknown -> running -> done | exited
  • unknown: the job directory exists but no task data has been written to the file system yet.
  • running: the runner process is alive.
  • done: the job finished; job_status.report carries the final counts.
  • exited: the runner process died without finishing (crash, kill, or stop_job). Progress is kept on disk; call resume_job to continue.

Disk cleanup

Job directories under MULTI_TASKS_MCP_JOB_ROOT are never deleted by this server — that is what makes stop/resume possible. Disk reclamation is the user's responsibility: delete job directories you no longer need.

License

MIT