claude-async
Summary: claude-async is a fire-and-poll MCP server that runs long Claude Code tasks as detached background jobs, so you can start work, get a jobId instantly, and poll for results without hitting Claude's tool-call timeout.
Start long-running Claude Code tasks with
claude_start(promptrequired, plusmodel,effort,workFolder,jobId) and get ajobIdback in milliseconds instead of blocking a tool call.Poll job status and output with
claude_check(jobIdrequired,tailBytes?) to get status, exit code, and a tail of stdout/stderr.List all jobs with
claude_jobsto see every known job and its current status.Survive restarts: jobs are detached and written to disk per job, so a dropped or restarted MCP bridge doesn't lose in-flight work — reconnect with the same
jobId.Dispatch across hosts (per README):
hostselects this machine or a registered remote host, with job ids prefixed by host and multi-host aggregation inclaude_jobs.Optional Discord notifications when jobs finish, and
initent-titled dispatch cards.
Note: the live schema diverges from the README — claude_start has no required host (README says required) and no intent, effort defaults to xhigh (README says medium) with extra low/xhigh/ultracode values, and claude_check reports orphaned rather than the README's died/timed_out.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@claude-asyncstart a long background job to refactor the codebase"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
claude-async
A fire-and-poll MCP server that lets Claude Code run long background jobs without hitting the Claude app's tool-call timeout.
Requirements: Node.js 18+ (20+ recommended; on a receiver, match the dispatcher's major version — see "Linux receiver") and the claude CLI installed and authenticated.
License: MIT
The problem
The Claude desktop app caps how long any single MCP tool call can run — roughly 60s per call, with a ~4–5 minute transport ceiling. A long Claude Code task (a big refactor, a multi-step build) outlives that window, the call drops, and you lose the in-flight work and start over.
Related MCP server: agent-bridge-mcp
The fix
claude-async spawns Claude Code as a detached background process and hands back a jobId in milliseconds. You poll for results whenever you like.
No single tool call lives long enough to time out.
Jobs are detached, so they survive a bridge restart — reconnect with the same
jobId.Output and exit status are written to disk per job, so nothing is lost.
Three tools: claude_start, claude_check, claude_jobs.
Set it up with Claude
Paste this prompt to Claude Code, or to Claude in the desktop app with this repo open. It stands the server up end to end and verifies it:
You're installing the `claude-async` MCP server from this repository. Work through
these steps in order and report the result of each. If any step fails, stop and show
me the exact error — do not continue.
1. Confirm prerequisites: `node -v` (must be 18+) and `claude --version` (the Claude
CLI must be installed and authenticated).
2. From the repo root, run `npm install`.
3. Verify the fire-and-poll plumbing without needing a live model:
`node claude-async-server.mjs --selftest`. It must report the detach → poll → exit
cycle passing.
4. Register the server with Claude Desktop by running `node register-desktop.mjs`.
(On Windows Store / MSIX installs this writes to the virtualized config path that
the in-app "Edit Config" button does NOT open — that mismatch is a known
silent-failure trap.) If you are not on Windows, add the config block from the
"Manual setup" section of the README instead.
5. Tell me to fully quit and relaunch Claude Desktop, then confirm `claude-async`
appears with status `running` under Settings → Connectors.
6. Smoke-test the round trip: call `claude_start` with the prompt "print hello world",
take the returned `jobId`, and poll `claude_check` until `status` is `completed`
and `exitCode` is 0. Show me the output.Manual setup
If you'd rather not use the prompt above, or you're not on Windows:
Install dependencies:
npm installVerify the plumbing:
node claude-async-server.mjs --selftestRegister the server by adding it to your Claude Desktop config file:
Windows (Store / MSIX install):
%LOCALAPPDATA%\Packages\Claude_pzs8sxrjxfjjc\LocalCache\Roaming\Claude\claude_desktop_config.json(The in-app "Edit Config" button opens%APPDATA%\Claude\instead, which the Store build does not read. Edit the path above, or just runnode register-desktop.mjs.)Windows (standard install):
%APPDATA%\Claude\claude_desktop_config.jsonmacOS:
~/Library/Application Support/Claude/claude_desktop_config.json
Add:
{
"mcpServers": {
"claude-async": {
"command": "node",
"args": ["/absolute/path/to/claude-async/claude-async-server.mjs"]
}
}
}Fully quit and relaunch Claude Desktop — closing the window is not enough. The server should then show as
running.
Tools
Tool | Input | Returns |
|
|
|
|
|
|
| — | every job on this host and every |
status is one of running | completed | failed | died | timed_out. completed is
reported only when the job exited with code 0; a non-zero exit is failed. died means the
process exited without recording a result (crash, SIGKILL, pid recycled to a foreign process); timed_out means no heartbeat for longer than CLAUDE_ASYNC_JOB_TIMEOUT_MS.
claude_check also includes an exit field when the runner's exit.json record (see
job-runner.mjs) exists in the job's dir: the parsed { exitCode, exitSignal, exitReason, spawnError?, endedAt, stderrTail, stdoutTail, usageLimitSuspected } object, letting a dead or
failed job be attributed without opening the job dir. It is omitted (never null) for jobs that
predate exit.json, and degrades to { error: "unparseable exit.json" } if the file is missing,
partial, oversized (>256 KiB), or otherwise unparseable. This is purely additive: it never affects
the status classification above.
died jobs additionally get a diedCause field, derived from exit: "spawn-error" (the CLI
never started), "signal" (killed by a signal), "exit" (the CLI exited with a code, but the
runner never recorded exit_code — e.g. killed in the narrow window between writing exit.json
and writing exit_code), or "unknown" (no exit.json at all — a job that predates it, or a
runner that died before writing one — or an unparseable one). diedCause never changes status;
it only explains a died job that exit couldn't otherwise attribute.
Field names are camelCase throughout — it's
jobId, notjob_id.
Configuration
Optional environment variables:
Variable | Default | Purpose |
|
| Path to the |
|
| Where per-job logs and exit codes are stored |
|
| Default working directory for jobs |
|
| Model used when |
|
| Reasoning effort used when |
|
| Where |
| unset |
|
|
| Max running jobs on this host ( |
|
| Max starts per rolling 60s on this host ( |
Discord notifications
Optional, best-effort: when a job's finish() writes exit.json/exit_code, the runner tries a
single bounded (5s) POST to a Discord webhook so you see a phone notification when a job ends.
Nothing about this can delay, change, or fail the job's recorded result — it runs after those
two files are already on disk, is wrapped in its own try/catch, has no retries, and is awaited
with a hard timeout so the runner process can't hang waiting on it. If it's not configured, or the
webhook is unreachable, the job finishes exactly as it would without this feature.
1. Create a webhook in Discord. Pick (or make) a channel — a private one is recommended, since the notification's optional headline line is pulled verbatim from job stdout. Channel settings → Integrations → Webhooks → New Webhook → copy its URL.
2. Write ~/.claude-async/notify.json on each host that should notify (CLAUDE_ASYNC_CONFIG_DIR
overrides the directory, same as hosts.json) — this repo never creates this file:
{ "discord": { "webhookUrl": "https://discord.com/api/webhooks/<id>/<token>", "includeHeadline": true } }Missing file, missing/blank
discord.webhookUrl, or unparseable JSON all mean the feature is off — no error. Config is read fresh at the end of every job, never cached, so editing it takes effect on the next job with no restart needed.includeHeadline(defaulttrue): when true, the message's second line is the first non-empty line of the job's own stdout log (out.log, read from the top — job reports conventionally put their verdict there) that isn't a markdown heading (#…), a code-fence marker (```), a bare markdown label (only list/number/emphasis markers around a short label, such as1.,- **Report**,**Summary:**, or a---rule), or a line containing a filesystem path (Windows drive paths likeC:\orD:/, UNC\\server, absolute POSIX paths like/home/or/c/Users/). It is truncated to 200 characters, with@,`,<,>backslash-escaped so it can't render as a mention or break message formatting. Falls back to the tail of stdout, under the same rules, whenout.logis missing/unreadable or has no qualifying line near the top; if the tail has no qualifying line either, the headline is omitted. Set itfalsefor a status-only ping.The message is plain text (
content) withallowed_mentions: {"parse": []}, so nothing in job output — including that headline — can ever ping@everyone/@here/a user/a role. It carries a status marker ([OK]/[FAIL]/[SIGNAL]/[SPAWN-ERROR]), host, a title, exit code, and duration. It never includes the prompt, stderr, environment, tokens, or file paths.Title: the ping's title is
claude_start's dispatchintentwhen one was given for the job (the same text the dispatch card's title shows), otherwise the jobId.
3. Lock the file down — it holds a webhook URL, which is a credential (posting to it needs no auth beyond the URL itself):
icacls "$env:USERPROFILE\.claude-async\notify.json" /inheritance:r /grant:r "${env:USERNAME}:F"chmod 600 ~/.claude-async/notify.json # Linux receiverPer-host. Notifications are per executing host, not global: each machine that runs jobs needs
its own notify.json (they can point at the same webhook, or different channels/webhooks).
Send a test ping once configured, by dispatching any trivial job and watching the channel —
e.g. claude_start with prompt "echo ok" (or from a shell on the host, a job that just prints a
line and exits 0) — then claude_check to confirm the job also completed normally.
Multi-host (registry-driven)
Which hosts exist is configuration, not code. Each machine's hosts.json is its registry:
the known hosts are this machine (localHost) plus every key of hosts, and a host can be
dispatched to when it has an entry with a url and a token. Adding a host is a config change
on each machine, never a code change. claude_start requires host (no default):
| What happens |
this machine ( | runs locally, no network hop |
a key of | forwarded to that host's host API over Tailscale |
anything else | error: |
The topology is one-way by config: a machine that appears in nobody's registry (the laptop) can never be dispatched to, and needs no special case in the code. A dispatcher lists the receivers it may send to; a receiver lists nobody unless it also dispatches.
Job ids are <host>.<descriptor>-YYYYMMDD-<8 random [a-z0-9]>, minted by the executing host. The
prefix is any valid host name (below). claude_check requires the prefix to be a known host and
otherwise returns an error naming the prefix and the known hosts (it never falls back to a local
lookup). Older un-prefixed ids have no dot and are still checked locally. The forwarder stores
nothing: the job record lives only on the executing host, and claude_check / claude_jobs ask it
live; claude_jobs asks every registry host that has a url.
Config (user profile, never the repo; CLAUDE_ASYNC_CONFIG_DIR overrides the directory):
~/.claude-async/hosts.json(every machine). Validated on every load; a file that fails validation is an error naming the file and the field, andclaude_startrefuses until it is fixed.Field
Required
Rule
localHostyes
this machine's name; matches
^[a-z0-9][a-z0-9-]{0,31}$(no dots:.is the job-id separator; no uppercase; not a Windows device name:con prn aux nul com1-com9lpt1-lpt9; it becomes an id prefix and part of dir names)receiverno (default
false)boolean;
truelets this machine runhost-api.mjshostsno
map name ->
{ "url", "token" }; every name matches the pattern above and is notlocalHost;urlis an http or https URL whose host is an IPv4 literal in the Tailscale range 100.64.0.0/10 (a receiver only binds a Tailscale address; hostnames, IPv6, loopback, RFC1918 and public addresses are rejected);tokenis a non-empty stringcapsno
object;
maxConcurrentandmaxStartsPerMinute, each optional and an integer >= 1 (no strings, 0, negatives or floats); unknown keys are an error;{}means the defaults (4 and 6)A dispatcher (here the laptop, which sends to two receivers and is itself in nobody's registry):
{ "localHost": "laptop", "hosts": { "claunker": { "url": "http://100.x.y.z:7850", "token": "<token from claunker's --new-token>" }, "ha": { "url": "http://100.a.b.c:7850", "token": "<token from ha's --new-token>" } }, "caps": { "maxConcurrent": 4, "maxStartsPerMinute": 6 } }A receiver (here
ha;hostsis empty because it dispatches to nobody):{ "localHost": "ha", "receiver": true }With no
hosts.json,claude_startrefuses with an error naming the file; un-prefixedclaude_checkstill works.~/.claude-async/api.json(receivers only):{ "port": 7850, "tokenSha256": "<hex>", "bindAddress": "100.x.y.z" }. Holds only the token's sha256;bindAddressis optional (required only if more than one Tailscale address is present).~/.claude-async/last-seen.json: last successful contact per remote host, used only for theunreachable (last seen …)row.
Tool schema and restarts. host is a required enum built when the bridge starts from the
registry (localHost plus the hosts keys), and its description lists the names. That list is
advertised once, so to add a host: edit hosts.json and restart the bridge. Routing re-reads the
file on every call and validates the requested host against the live registry too, so a stale enum
can never route to a host that has since been removed (that call errors with the known hosts).
Adding a receiver (both sides):
On the receiver: install this repo and Node, create its
hosts.json{ "localHost": "<name>", "receiver": true }, runnode host-api.mjs --new-token(prints the token once; stores only its hash inapi.json), then startnode host-api.mjs(a service on Linux, see below). It binds only to a Tailscale address and refuses to start ifreceiveris nottrue.On each machine that should dispatch to it: add
"<name>": { "url": "http://<tailscale ip>:7850", "token": "<token>" }underhosts, then restart the bridge (Claude Desktop) so thehostenum includes it.Do not add the receiver to a machine's registry unless that machine should be able to send to it; do not list a dispatcher-only machine (the laptop) anywhere.
Host API (host-api.mjs, receivers only). POST /v1/start, GET /v1/check?jobId=,
GET /v1/jobs, all behind Authorization: Bearer <token> (bare 401 on failure, checked before
anything is read or written). It rejects a start whose host is not this receiver's localHost. It
binds only to a Tailscale address (100.64.0.0/10) that is present
on an interface and refuses to start otherwise; it never binds 0.0.0.0 or loopback. Starts go
through the same guarded startJob() and the same launcher queue as the MCP bridge.
node host-api.mjs --new-token # once: stores sha256 in api.json, prints the token once
node host-api.mjs # run (foreground)Linux receiver (Debian LXC on Proxmox, e.g. ha)
Nothing in this repo has ever run on Linux. The POSIX launch path (
spawndetached +unref) and the receiver code are exercised by the test suite only on Windows so far. On a new receiver, runnpm run test:multihostfirst, before pointing anything at it, and read any failure as a real finding.
Non-root service user, unprivileged container. The
claudeCLI refuses--dangerously-skip-permissionsas root, and every job is started with it. Create a dedicated user (the sample unit usesclaude) inside an unprivileged LXC — not a privileged one, and not a user on the Proxmox host itself, which also hosts the HA OS VM. The service user's home should be the only writable work area (CLAUDE_ASYNC_DEFAULT_CWDand the job-log dir both live under it); install the CLI for that user and log it in.The
claudebinary. Put it on the service'sPATH, or setCLAUDE_CLI_PATHto its absolute path (a systemd unit does not read your shell profile, so set one of the two explicitly).Node. Match the dispatcher's major version, Node 24 (
node --versionon the dispatching machine tells you which);/usr/bin/nodein the sample unit.npm ciin the checkout, notnpm install, so the receiver gets the exact locked versions the dispatcher tested against.Tailscale in the container.
host-api.mjsbinds only to a Tailscale address that is on a local interface. An LXC needs the TUN device passed through fortailscaledto create that interface (userspace-networking mode has no interface, so the API would refuse to start). Runtailscale upinside the LXC itself (or whichever host actually runs the receiver process) — not only on the Proxmox host, and not as the Home Assistant OS Tailscale add-on, which is a different node on the tailnet. This is Proxmox-side setup, not exercised here.Tailnet ACL. Add a rule allowing the dispatcher to reach the receiver on its port, e.g. (Tailscale ACL JSON)
{"action": "accept", "src": ["claunker"], "dst": ["ha:7850"]}, using whatever tags or host names your tailnet ACL already keys on.Service.
deploy/claude-async-api.serviceis a sample unit:User=claude,WorkingDirectory=/home/claude/code/claude-async,EnvironmentforHOME,PATH,CLAUDE_CLI_PATH,CLAUDE_ASYNC_DEFAULT_CWD,Restart=on-failure,KillMode=process, andNoNewPrivileges=true/PrivateTmp=true(hardening that doesn't touch job execution; see the unit's own comments for whyProtectHome/ProtectSystem=strictare deliberately not set). The POSIX launch isspawn(detached)+unref, so running jobs stay in the unit's cgroup; the defaultKillMode=control-groupwould kill them whenever the API restarts. Copy it to/etc/systemd/system/, thensystemctl daemon-reload && systemctl enable --now claude-async-api. The unit has not been run.Optional:
CLAUNKER_JOBCARD_CMDoverrides the dispatch-card command. When it is unset and the default command (the claunker-hermes venv under~/code/claunker-hermes) does not exist, the host has no card command: card minting and closing are skipped quietly (one debug line on stderr, noUNCARDEDnote, jobs still run). Any other failure still fails open with anUNCARDEDnote in the start response, because it is a real misconfiguration: an explicitCLAUNKER_JOBCARD_CMDthat cannot be run (including a missing exe), a non-zero exit, or a timeout. Set it only if the receiver has a card command.Config:
~/.claude-async/hosts.json{ "localHost": "ha", "receiver": true }for the service user, thennode host-api.mjs --new-tokenas that user, and addhato the dispatchers' registries (steps above).Token storage.
--new-tokenprints the plaintext token once and stores only its sha256 inapi.json; that printed value is the only copy. Don't leave it sitting in a shell history file or a scratch note — put it in a password manager (e.g. Bitwarden) long-term, andchmod 600any file you do stage it in temporarily (and the dispatcher'shosts.json, since it holds the plaintext token) before it leaves your terminal.
Guard (one, server-side). Every start on the executing host, local or via the API, is
checked for caps: max concurrent running jobs (default 4) and max starts per rolling minute
(default 6). Over a cap it is rejected with a clear error and no ticket. Duplicate ids are rejected
at create time. A preflight checks that the Claude binary and workFolder exist (errors name the
host). Success says exactly preflight passed, execution unverified: the runner may still fail,
and records that in the job. CLAUDE_ASYNC_DEPTH / X-Claude-Async-Depth (depth > 1 rejected)
is an accident guard only, not a security control. Anyone can send any value.
The guard fails closed on corrupt state. A job dir whose meta.json is missing or unreadable
still counts as running if it changed in the last 2 minutes; an older one is not counted but is named
in a warnings array on the start response (repair or delete it). A missing or corrupt
.start-ledger.json is rebuilt from the job dirs created in the last 60 s, never treated as empty.
State files (meta.json, tickets, the ledger, last-seen.json, api.json) are written
atomically (temp file + rename), so a crash mid-write cannot truncate them.
How it works
claude_start writes a small job record and spawns a detached worker
(job-runner.mjs) that runs the claude CLI, streaming stdout/stderr to that job's
log files and recording the exit code when it finishes. The parent returns the jobId
immediately and the worker is unref'd, so it keeps running even if the MCP bridge is
recycled. claude_check simply reads that job's status and log tail from disk — also
instant. Because state lives on disk rather than in the live connection, a dropped or
restarted bridge never costs you a running job.
On Windows, "detached" alone isn't enough for that guarantee. detached: true only
puts the worker in a new process group — it does not remove it from whatever Windows Job
Object the bridge itself is running in, and Claude Desktop runs MCP servers in a job with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE set (confirmed via IsProcessInJob +
QueryInformationJobObject during the 2026-09-09 investigation on fix/win32-detach), so a
naively-detached worker can die when the bridge does. An initial fix shelled out to
win32-breakaway.ps1 (CreateProcessW with CREATE_BREAKAWAY_FROM_JOB), but runners still
self-reported job membership afterward and were still observed being hard-killed. The launch
path now defaults instead to a small Windows Task Scheduler-based launcher
(job-launcher.mjs, registered as the ClaudeAsyncRunner task by job-core.mjs's
ensureLauncherTask()) that gives the worker an ancestor — the Task Scheduler service — that
was never inside Claude Desktop's process tree or job to begin with; win32-breakaway.ps1 is
kept as an automatic fallback if the task can't be registered or triggered. See
RUNBOOK.md's "Task Scheduler launcher" section for the full design and its verification, and
test/survival.mjs for the tests (three independent kill mechanisms plus a launcher claim-race
test).
Gotchas
Server shows
runningbut tools don't respond: fully quit and relaunch the app; closing the window doesn't reload MCP servers.Windows Store install, config edits ignored: you're editing the wrong file — see the MSIX path under Manual setup, or run
register-desktop.mjs.Very large
claude_startprompts fail on Windows (ENAMETOOLONG): the prompt is passed as a CLI argument, so keep it modest and have the job read large inputs from a file instead.
Known issues
claude_checktrusts the stored job record. If the bridge restarts between job completion and record close (e.g. a Claude Desktop swap), the record showsrunningforever while the detached job has finished and its work landed. Observed 2026-07-04 (jobkanbantt-remember-token-optin: commit pushed at 01:52Z, record never closed). Fix direction:claude_checkshould re-stat the pid and reap exit state from the job directory rather than trusting the record.
License
MIT — see LICENSE.
Available Tools
3 toolsclaude_checkA
Check a background job's status and recent output. Returns status (running | completed | failed | orphaned), exit code, and a tail of stdout/stderr.
| Name | Required | Description | Default |
|---|---|---|---|
| jobId | Yes | ||
| tailBytes | No | Bytes of stdout/stderr to return (default 8000). |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries full burden. It discloses the return values but does not explicitly state that the tool is non-destructive (read-only) or mention any prerequisites, side effects, or rate limits. The 'Check' verb implies read-only, but this is not confirmed.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is two sentences, front-loaded with the core purpose, and contains no redundant or extraneous information. Every word contributes to clarity.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool has 2 parameters and no output schema, the description adequately conveys the action, return values, and the optional tailBytes parameter. It could be slightly more complete by mentioning the default tail size or that it returns only the last portion of output.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 50% with only tailBytes having a description. The description adds context for jobId (identifies the job) and aligns with tailBytes. However, the description does not elaborate on the format or constraints of jobId beyond the schema's 'string' type.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool checks a background job's status and recent output, specifying the returned fields (status, exit code, tail of stdout/stderr). It distinguishes from siblings by focusing on checking a specific job rather than listing or starting jobs.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description implies usage for checking a known job, but does not provide explicit guidance on when to use this tool versus claude_jobs (e.g., to list all jobs) or claude_start (to start a new job). No when-not-to-use or alternatives are mentioned.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
claude_jobsB
List all known background jobs with their current status.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Given no annotations, the description carries the full burden. It describes a read-only operation listing jobs with status, which is straightforward, but lacks details on permissions, limitations, or output format.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is a single short sentence that immediately conveys the tool's purpose. Every word contributes, no redundancy.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a tool with no parameters and no output schema, the description is adequate but minimal. It could hint at what 'current status' includes or list typical status values to improve completeness.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
There are zero parameters, so schema coverage is 100% trivially. Per guidelines, no parameters yields a baseline of 4.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description uses a specific verb 'list' and specifies the resource 'background jobs' with status. It clearly states what the tool does but does not differentiate from siblings claude_check and claude_start.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
No guidance is provided on when to use this tool versus alternatives. The description only states the listing action without context or exclusions.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
claude_startA
Start a Claude Code task as a detached background job and return a jobId immediately. Use for any work that might run longer than ~30s. Poll with claude_check.
| Name | Required | Description | Default |
|---|---|---|---|
| jobId | No | Custom job id; otherwise one is generated. | |
| model | No | Optional --model override, e.g. claude-opus-4-8 / claude-sonnet-4-6. | |
| effort | No | Reasoning effort; default xhigh. "max" = highest reasoning; "ultracode" = xhigh plus standing dynamic-workflow orchestration (parallel subagents). | |
| prompt | Yes | The task for Claude Code. Include CWD context if it does file/git work. | |
| workFolder | No | Directory to run in (default: $HOME or CLAUDE_ASYNC_DEFAULT_CWD). |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the burden. It discloses that the task runs detached and returns a jobId, but does not detail error handling, side effects, or behavior on duplicate jobIds. Acceptable but minimal.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two sentences, no fluff, front-loaded with purpose and usage. Every sentence adds value.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given no output schema, the description could elaborate on the return structure (e.g., jobId format). It covers the main points for a start tool, but the mention of polling and siblings could be more integrated.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the baseline is 3. The description adds no additional meaning beyond what the parameter descriptions already provide. It mentions the prompt and workFolder in passing but without extra detail.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool 'start[s] a Claude Code task as a detached background job' and mentions it returns a jobId immediately. It distinguishes from siblings by referencing polling with claude_check and implicitly contrasting with claude_jobs.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description explicitly says to use this tool for work that might run longer than ~30s and directs polling with claude_check. While it doesn't enumerate when not to use it, the guidance is clear enough for typical use.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
3 tool updates
v1.0.0- First observed
claude_check - First observed
claude_jobs - First observed
claude_start
TDQS
Scored across 3 tools
Each tool serves a distinct purpose: starting a job, checking a specific job, and listing all jobs. No overlap in functionality.
All tools share the 'claude_' prefix, but the second part mixes verbs (check, start) and a noun (jobs). Consistent prefix but slightly inconsistent in part-of-speech.
Three tools is well-scoped for managing async background jobs, covering the essential operations without bloat.
Covers starting, checking, and listing jobs. Missing a cancel/abort tool, but the core workflow is functional.
Maintenance
Related MCP Connectors
Nifty's MCP server — exposes tasks, projects, messages, and files as tools for AI agents.
Remote MCP server for supportsheep: run AI interviews and manage support content for your blog.
MCP server for progressive tool usage at any scale (see https://klavis.ai)
MCP server for the FFmpeg Micro video transcoding API — create, monitor, download transcodes.
Related MCP Servers
- AlicenseAqualityDmaintenanceA Model Context Protocol server that enables running and managing long-running background tasks (like development servers, builds) from within Claude Desktop or other MCP-compatible clients.66 npm3ISC
- AlicenseAqualityDmaintenanceAn MCP-only server that launches AI CLI tasks (Claude, Codex, Gemini, Forge, OpenCode) as background subprocesses and manages them via PID for status, output, and lifecycle control.913 npmMIT
- AlicenseAqualityFmaintenanceMCP server for running external coding agents as background tasks inside Claude Code. Supports multiple backends including Codex, Grok, GLM, DeepSeek, and more.7MIT
- FlicenseNot gradedqualityCmaintenanceA minimal MCP server for running Python scripts on a remote machine over HTTP, exposing tools to Claude for script discovery, execution, and job status tracking.-