Back to MCP Servers

Jobd

Self-hostable GPU-aware job broker for your own machines. The MCP server exposes the queue as nine tools (submit, status, logs, list, cancel, preempt, workers, job-get, worker-delete) so an agent can dispatch long-running or GPU jobs that outlive the session, route them by VRAM …

code-executionagent
By musharna
3Updated 3 days agoPythonMIT

Installation

pip install "jobd[mcp]"

Configuration

{
  "mcpServers": {
    "jobd": {
      "command": "npx",
      "args": ["-y", "jobd"]
    }
  }
}

How to use

  1. Run the installation command above (if needed)
  2. Open your Claude Code settings file (~/.claude/settings.json)
  3. Add the configuration to the mcpServers section
  4. Restart Claude Code to apply changes
<div align="center">

jobd

CI PyPI Python Glama GHCR License: MIT

A self-hostable, GPU-aware job broker for your own machines — with native MCP/agent integration.

Like task-spooler or pueue, but across all your machines — and VRAM-aware.

</div> <!-- mcp-name: io.github.musharna/jobd --> <p align="center"> <img src="https://raw.githubusercontent.com/musharna/jobd/main/docs/assets/demo.svg" alt="jobd in action: job fleet status shows four workers and their versions; a GPU job routes to the worker with enough free VRAM and streams back; a stdin batch submits two jobs at once; job logs -f follows one to completion" width="100%"> </p>

You have a couple of boxes with GPUs — a workstation, a server, maybe a laptop — wired together over Tailscale or a LAN. You want to fire off training runs, data pipelines, and long batch jobs from anywhere, have them land on whichever machine actually has the VRAM free, survive across sessions, and get preempted cleanly when something more important shows up. You don't have a cloud, a Kubernetes cluster, or a Slurm install, and you don't want one.

jobd is that missing piece: a small broker that turns a handful of personal machines into a single queue — and an LLM agent can drive it directly.

# from any machine on your tailnet:
job submit --project myproj --gpu --vram-required 16 --wait -- python train.py
# → routed to whichever worker has ≥16 GB VRAM free, streamed back to your terminal

Why it exists

Most schedulers assume a datacenter. The lightweight ones that don't (a bare nohup, a tmux session, an ssh-and-pray script) give you nothing: no queue, no VRAM-aware routing, no preemption, no record of what ran where. jobd fills the gap between "ssh in and run it" and "stand up Slurm":

  • VRAM-fit routing. The broker matches each job against live worker capacity (free VRAM / RAM / CPUs, capability tags, arch/OS) and dispatches to a worker that actually fits — instead of you guessing which box is free.
  • Preempt + checkpoint. A higher-priority job can preempt a running one: the worker sends SIGTERM, the workload gets a grace window to checkpoint, then SIGKILL. A preempted job reaches a terminal preempted state with a durable checkpoint to resume from — it isn't silently re-run. (See docs/preemption.md.)
  • Survives sessions. Submit, close your laptop, check back tomorrow. Jobs live in the broker, not your shell.
  • Agent-native. Ships a first-class MCP server so an LLM agent (Claude Code, etc.) can submit, monitor, and babysit jobs as tool calls — the thing most schedulers bolt on as an afterthought, if at all.
  • Yours. One broker process you run on a machine you own. No accounts, no egress, no per-GPU-hour billing. Tailnet-bound by default.

Why not just use…?

ToolWhat it gives youWhy jobd instead
nohup / tmux / ssh-and-prayRuns a command on one boxNo queue, no VRAM-aware routing, no preemption, no record of what ran where
task-spoolerA real job queue — on a single machinejobd queues across all your machines and routes by live VRAM/CPU fit
PueueThe best single-machine command queue daemonPueue's own README declares distributed execution out of scope — jobd is that missing layer, plus GPU awareness
HyperQueueMulti-machine task scheduling with HPC roots, single binaryHQ counts GPUs but doesn't track VRAM, and has no preemption/checkpoint contract or agent interface
SlurmDatacenter-grade schedulingHeavy to stand up and operate for 2–3 personal boxes; jobd is one process + a poller per host
SkyPilot / Modal / dstackProvision and run on clouds + your own machinesSkyPilot's "existing machines" mode installs a k3s cluster on your boxes; dstack wants Docker + passwordless sudo on every host. jobd is one process + a poller — no containers, no sudo, no K8s
RayA distributed-compute frameworkjobd is a job queue, not a programming model — submit any command, no code changes, GPU-fit routing built in

Closest in spirit are Pueue and task-spooler (single-machine by design) and HyperQueue (multi-machine, HPC-shaped). jobd's niche is the 2–5-GPU homelab: multi-machine live VRAM-fit routing + preempt/checkpoint + a native MCP interface — a combination none of the above offers — with nothing heavier than a Python process per host.

Architecture

<p align="center"> <img src="https://raw.githubusercontent.com/musharna/jobd/main/docs/assets/architecture.svg" alt="Architecture: job CLI, jobd-mcp MCP tools, and HTTP/SSE clients talk to the jobd broker (FastAPI — queue, VRAM matcher, priorities, SQLite) over one tailnet; the broker dispatches via long-poll claims and heartbeats to workers A (24 GB GPU), B (8 GB GPU), and C (CPU-only)" width="640"> </p> <details> <summary>Diagram source (mermaid)</summary>
flowchart TD
    CLI["job CLI"]:::client --> B
    MCP["jobd-mcp<br/>MCP tools"]:::client --> B
    API["HTTP · SSE"]:::client --> B
    B["<b>jobd broker</b> — FastAPI<br/>queue · matcher · priorities · SQLite"]:::broker
    B <-->|poll · dispatch| WA["worker A<br/>24 GB GPU"]:::worker
    B <-->|poll · dispatch| WB["worker B<br/>8 GB GPU"]:::worker
    B <-->|poll · dispatch| WC["worker C<br/>CPU-only"]:::worker
    classDef client fill:#1f2937,stroke:#4b5563,color:#e5e7eb;
    classDef broker fill:#0e7490,stroke:#155e75,color:#ecfeff;
    classDef worker fill:#14532d,stroke:#166534,color:#dcfce7;
</details>

Workers poll the broker (pull model — no inbound connection to a worker); the broker matches each job against live capacity and hands it back on the poll. One broker process, one poller per host.

  • Broker — a FastAPI + SQLite service. Holds the queue, runs the matcher, resolves per-project priorities and defaults, exposes a small HTTP API and an SSE stream. Single source of truth.
  • Workers — lightweight polling agents, one per host. Each advertises live capacity via heartbeat, claims jobs it can run, executes them (shell=False, no shell-injection surface), streams logs back, and honors preemption signals.
  • Clients — the job CLI, the jobd-mcp MCP server, or anything that speaks the HTTP API.

Install

pip install jobd               # broker + CLI
pip install "jobd[mcp]"        # adds the MCP server
pip install "jobd[worker]"     # adds the worker daemon (jobd-worker)

Requires Python ≥ 3.11. Everything ships in the one jobd package: the broker (jobd), the CLI (job), the MCP server (jobd-mcp), and the worker (jobd-worker). The worker's runtime deps (httpx, psutil, pyyaml, nvidia-ml-py) live behind the [worker] extra since they're only needed on machines that actually run jobs. scripts/install-worker.sh sets a worker up under ~/jobd-worker with its own venv and a generated config.

Quickstart (single host)

# 1. start the broker (binds 127.0.0.1:8765 by default)
JOBD_ALLOW_NO_AUTH=1 jobd          # no-auth is fine for a loopback-only broker

# 2. in another shell, install + start a worker pointed at it
pip install "jobd[worker]"
JOBD_URL=http://127.0.0.1:8765 JOBD_WORKER_HOST=local jobd-worker

# 3. submit a job and wait for it
job submit --project demo --wait -- echo hello
job list
job logs <id>

For a real multi-host deployment (Docker broker + systemd workers, Tailscale binding, shared auth token), see docs/security.md and the templates in docker-compose.yml and scripts/. Adding a worker to a running fleet is one command:

job fleet add user@newbox      # ssh in, install pinned to the broker's version,
                               # wire systemd units + the self-update timer,
                               # verify it registers. `job fleet status` shows drift.

Day-2 operations (health, draining a worker, upgrades, token rotation, backups) are in docs/runbook.md.

Supported platforms

Python 3.11+ everywhere.

ComponentLinuxmacOSWindows
Broker (jobd)☑️☑️ (WSL recommended)
CLI (job) / MCP (jobd-mcp)☑️☑️
Worker (jobd-worker)✅ full⚠️ degraded⚠️ degraded

✅ = CI-tested (the test matrix runs on Linux). ☑️ = pure-Python and expected to work, but not exercised by CI — please file an issue if something is broken there.

The worker runs its best on Linux with a systemd user instance: memory caps, process reaping, and preemption use systemd-run --user scopes and cgroups. On non-systemd hosts the worker still executes jobs, but silently drops those guarantees — fine for a single trusted box, not for hard resource isolation. GPU features need NVIDIA + nvidia-ml-py. The broker, CLI, and MCP server are pure-Python and portable.

CLI

job submit -p PROJ [--gpu] [--vram-required N] [--needs TAG]... [--count N | --sweep K=v1,v2]... [--wait] -- CMD...
job list [--state STATE] [--project P] [--array A<id>]   # queue + recent jobs
job status ID | A<id> [--watch]             # one job, or an array's aggregate
job logs ID [-n BYTES]                      # tail captured output
job wait ID                                 # block until terminal
job cancel ID  /  job preempt ID            # stop a job
job workers                                 # fleet snapshot + health
job projects list | set NAME PRI | nudge NAME DELTA
job audit [--project P] [--since 24h]       # event history

job submit --explain dry-runs the resolution (priority, profile, project defaults, host pin) and prints the effective config without enqueuing anything.

Job arrays

Submit N jobs from one template with `

View source on GitHub