Skip to content
Get started
Get started
MCP Server: Let AI Agents Drive the Queue
View Markdown
guide · mcp

Your queue, driven by agents.

Connect Claude, Cursor or any MCP client to bunqueue, then ask in plain language: add jobs, schedule crons, retry failures, check on your queues. No code to write.

MCP (Model Context Protocol) is the standard way for AI agents to call tools. bunqueue includes an MCP server, bunqueue-mcp, with 75 tools that cover the whole queue: jobs, queues, crons, retries, limits, flows, workers, webhooks and monitoring.

  1. Install. bunqueue-mcp ships inside the bunqueue package. The MCP SDK is a separate, optional dependency that only the MCP server needs.

    bun add -g bunqueue @modelcontextprotocol/sdk
  2. Connect your client.

    claude mcp add bunqueue -- bunx --package=bunqueue bunqueue-mcp
  3. Ask. For example: “Add a job to the emails queue, then show me the queue.”

That is all. The queue runs inside the MCP server, in memory, so jobs disappear when the session ends. To keep them, add "env": { "DATA_PATH": "/absolute/path/to/bunq.db" } to the server config. Use an absolute path: the client, not you, chooses the directory the MCP server starts in. To manage a bunqueue server you already run, see where the queue lives.

You sayThe agent uses
”Add 3 notification jobs: push, email, sms”bunqueue_add_jobs_bulk
”Schedule session cleanup every day at 3 AM Rome time”bunqueue_add_cron with timezone
”Limit notifications to 100 per minute”bunqueue_set_rate_limit with duration
”Which billing jobs failed today, and why?”bunqueue_get_dlq_stats, then bunqueue_get_dlq
”Retry everything in the dead letter queue”bunqueue_retry_dlq
”Validate the payment, then send the receipt, then update inventory”bunqueue_add_flow_chain
”Why are jobs stuck in the emails queue?”The bunqueue_debug_queue prompt

You never call tools yourself: the agent picks them. The full list is in the tool reference.

Your AI client Claude Code, Claude Desktop, Cursor, Windsurf Any client that speaks MCP
bunqueue-mcp 75 tools, 3 prompts, 5 resources
  • toolsets
  • confirmations
  • decision models
  • Embedded default Inside the MCP server In memory, or a SQLite file with DATA_PATH
  • TCP BUNQUEUE_MODE=tcp Your bunqueue server, port 6789 SQLite or PostgreSQL, shared with your workers

The MCP server works in one of two modes. All 75 tools work in both, including HTTP handlers: in TCP mode their workers connect to your server and process its jobs, and the MCP process never opens a local database, even with DATA_PATH set. Two differences remain: bunqueue_get_stats reports push and pull rates in TCP mode, and prioritized, waiting-for-children, lifetime and cron totals in embedded mode; and workflow approvals need TCP.

Embedded (default)TCP
The queue runsInside the MCP serverOn your bunqueue server
Good forTrying things, one machineManaging the queues your workers use
Jobs are keptIn memory, or in a SQLite file with DATA_PATHBy your server (SQLite or PostgreSQL)
SetupNothingSet BUNQUEUE_MODE=tcp and the address

To connect to your server:

{
"mcpServers": {
"bunqueue": {
"command": "bunx",
"args": ["--package=bunqueue", "bunqueue-mcp"],
"env": {
"BUNQUEUE_MODE": "tcp",
"BUNQUEUE_HOST": "your-server.com",
"BUNQUEUE_PORT": "6789",
"BUNQUEUE_TOKEN": "your-auth-token"
}
}
}
}

An agent can add jobs, but it cannot run a worker. An HTTP handler fills that gap: the agent registers a URL on a queue, and the MCP server calls that URL for every job. The response becomes the job’s result. An error status or a timeout fails the job, which is then retried or moved to the dead letter queue like any other.

> Register a GET handler on "meteo" that calls the OpenWeather API
✓ bunqueue_register_handler Jobs in "meteo" will be processed via GET.
> Add a cron that pushes a job every 10 seconds
✓ bunqueue_add_cron name: "check-meteo", repeatEvery: 10000
  1. waiting A job is added By the agent, by a cron or, in TCP mode, by any app on your server
  2. active The handler takes it A worker inside the MCP server; in TCP mode it pulls from your server
  3. active It calls your URL GET POST PUT DELETE; POST and PUT send the job's data, or your fixed body. It waits up to timeoutMs
    • completed 2xx response The response is saved as the job's result
    • failed Error status, network error or timeout Retried with backoff while attempts remain, then moved to the dead letter queue

bunqueue_register_handler takes the queue, the url, the method (GET, POST, PUT or DELETE), and optionally headers, a fixed body (POST and PUT send it, or the job’s data when it is not set; GET and DELETE send no body) and timeoutMs (default 30000, from 1000 to 120000). In TCP mode the handler processes your server’s queue.

Use handlers when the work is a single HTTP call: polling an API, forwarding a webhook, a health check. For anything else, run a normal Worker: the agent still adds and schedules the jobs.

Three features, all off by default. You turn them on in the server config, and they work well together.

FeatureTurn it onWhat you get
Load only the tools you needBUNQUEUE_MCP_TOOLSETS=dynamicThe agent loads groups of tools on demand: about 1,300 to 4,600 tokens per request instead of about 16,000
Confirm destructive operationsBUNQUEUE_MCP_CONFIRM=destructiveOperations that delete data for good ask you first, with their exact impact
Decision modelsBUNQUEUE_MCP_DECISION_PROVIDERA small, fast model picks tools from plain language and double-checks destructive requests

The first two, together:

claude mcp add bunqueue \
-e BUNQUEUE_MCP_TOOLSETS=dynamic \
-e BUNQUEUE_MCP_CONFIRM=destructive \
-- bunx --package=bunqueue bunqueue-mcp

Every tool an agent can see costs context: the 75 descriptions add up to about 16,000 tokens on every request, before the agent reads your question. Toolsets cut that down.

BUNQUEUE_MCP_TOOLSETSThe agent sees
unset, or allAll 75 tools, up to 78 with the workflow tools (the default)
queues,dlqOnly those groups. Works with every client
dynamicA short catalog of the groups. The agent loads the ones it needs
dynamic,monitoringThe catalog, with monitoring already loaded

The groups are jobs, job-control, processing, queues, dlq, cron, limits, flows, workers, webhooks and monitoring (plus workflows when workflow tools are on).

In dynamic mode the agent starts with two tools: bunqueue_enable_toolsets, which loads groups, and bunqueue_call_tool, for clients that do not refresh their tool list after a group is loaded. The catalog alone costs about 850 tokens; with one group loaded, 1,300 to 4,600 depending on the group (about 1,800 for a typical group, close to 90% less than the full list; jobs and flows are the largest).

How well does the agent pick the right group?

We measured it with 72 requests, including paraphrases such as “let billing jobs run again” and requests in Italian. Given only the group descriptions, TypeSafe Jev picked the group with the right tool for 71 of 72 on the first run, and for all 72 after we reworded the one description involved in the miss (so the second score is not a blind result). Keyword search over the individual tools found the right one in its top 5 for only 53% of the held-out requests. The requests, the script and the results are in scripts/mcp-eval/.

With BUNQUEUE_MCP_CONFIRM=destructive, nine tools no longer act on the first call:

obliterate_queue, drain_queue, clean_queue, purge_dlq, cancel_job, clear_job_logs, delete_cron, remove_webhook, and retry_completed without a jobId.

They delete data for good, or re-run work that already completed. Retrying failed jobs from the dead letter queue is not on the list, because that is the normal fix for a failure, and neither is discard_job, which moves a job to the dead letter queue where it can still be retried.

Before running one of them, the server works out the impact, for example: Permanently delete queue “billing” and every job in it (1204 waiting, 0 prioritized, 0 delayed, 3 active, 18330 completed, 2 failed), including results and logs. Then:

  • If your client can ask you (it supports MCP elicitation), you see the impact and choose. A decline always wins, even if the agent tried to confirm on its own.
  • If it cannot, the call is refused with the impact. The agent must show it to you and, once you agree, call again with confirm set to the exact target: the queue name, job id, cron name, webhook id or execution id.

The confirmation covers the target. The other arguments are the ones the impact describes, so read it before you say yes. Every other tool works as before; with this setting, every tool is also labeled as read-only, destructive, idempotent or open-world for clients that use those hints.

Decision models: Jev, Clef, Clef-flash, Kev 9B, Laya

Section titled “Decision models: Jev, Clef, Clef-flash, Kev 9B, Laya”

A decision model is a small model that answers a typed question (pick one option, or say how likely something is) in milliseconds, instead of writing text. bunqueue can use one for two things:

  • Picking tools. With BUNQUEUE_MCP_TOOLSETS=dynamic, the agent gets bunqueue_find_tools: it describes what it needs and the model loads the right group. Helpful with smaller agent models.
  • A second check on destructive calls. With BUNQUEUE_MCP_CONFIRM=destructive and a client that cannot ask you, the agent must also pass your words (userRequest), and the model must agree that you asked for exactly that operation.

The model can only block a call, never approve one for you. It is not asked when you already confirmed in the client, and if it is unreachable or unsure, the call is refused.

ProviderModelsYou set
typesafeJev (jev-latest, jev-1.13.0)BUNQUEUE_MCP_DECISION_API_KEY
cloudflareClef, Clef-flash on Workers AIBUNQUEUE_MCP_DECISION_API_KEY, BUNQUEUE_MCP_DECISION_ACCOUNT_ID
systemoneAny compatible endpoint you host: Clef and Clef-flash (open weights), Kev 9B, Laya, DiffusionGemma JevBUNQUEUE_MCP_DECISION_URL, BUNQUEUE_MCP_DECISION_MODEL
{
"mcpServers": {
"bunqueue": {
"command": "bunx",
"args": ["--package=bunqueue", "bunqueue-mcp"],
"env": {
"BUNQUEUE_MCP_TOOLSETS": "dynamic",
"BUNQUEUE_MCP_CONFIRM": "destructive",
"BUNQUEUE_MCP_DECISION_PROVIDER": "cloudflare",
"BUNQUEUE_MCP_DECISION_MODEL": "clef-flash",
"BUNQUEUE_MCP_DECISION_ACCOUNT_ID": "your-account-id",
"BUNQUEUE_MCP_DECISION_API_KEY": "your-api-token"
}
}
}
}

Which one? On public benchmarks, Jev is best at knowing when not to call a tool (When2Call 80.97), the skill behind the destructive-call check; Clef is best at finding the right tool (ToolRet 69.19), the skill behind bunqueue_find_tools; Clef-flash is the fastest, about 39 ms at the median, roughly 13 times faster than Jev. A weaker model only makes the check stricter, never less safe. For a self-hosted model, set BUNQUEUE_MCP_DECISION_URL (for example http://127.0.0.1:8000/v1/systemone) and BUNQUEUE_MCP_DECISION_MODEL (for example kev-9b); Clef’s model card includes a systemone() helper that serves exactly this request format.

By default every client starts its own bunqueue-mcp process. To run one server that several clients share, start it in HTTP mode:

BUNQUEUE_MCP_TRANSPORT=http \
BUNQUEUE_MCP_HTTP_TOKEN=change-me \
bunx --package=bunqueue bunqueue-mcp
# bunqueue MCP server started (mode: embedded, transport: http, url: http://127.0.0.1:6791/mcp, ...)

Then connect each client to the URL with the token:

claude mcp add --transport http bunqueue http://127.0.0.1:6791/mcp \
--header "Authorization: Bearer change-me"

Each client gets its own session, and all sessions share one queue connection and one set of HTTP handlers. Add BUNQUEUE_MODE=tcp and the server address, as above, to manage your bunqueue server.

Good to know:

  • It listens on 127.0.0.1:6791. On any other address it refuses to start without BUNQUEUE_MCP_HTTP_TOKEN (a comma-separated list, so you can rotate tokens). A missing or wrong token gets 401.
  • There is no built-in TLS. To reach it from another machine, keep it on 127.0.0.1 behind a reverse proxy that handles TLS, and keep the token.
  • Sessions live in memory. After a restart, or 30 minutes of inactivity, a client gets Session not found and has to reconnect.
Reverse proxy, Host and Origin checks, limits

To block DNS-rebinding attacks from web pages, the server checks the Host header (and the Origin header when a browser sends one) and answers 403 to names it does not know. Behind a reverse proxy, list the public name in BUNQUEUE_MCP_HTTP_ALLOWED_HOSTS, and browser origins in BUNQUEUE_MCP_HTTP_ALLOWED_ORIGINS:

# Caddyfile (start the server with BUNQUEUE_MCP_HTTP_ALLOWED_HOSTS=mcp.example.com)
mcp.example.com {
reverse_proxy 127.0.0.1:6791
}

Behind nginx, set proxy_buffering off so streamed responses are not held back.

On loopback or 0.0.0.0 the server accepts localhost, 127.0.0.1 and [::1] on its port; an allowed host can be name (any port) or name:port. At most 100 sessions are open at once (BUNQUEUE_MCP_HTTP_MAX_SESSIONS; beyond that, 503), an idle session closes after BUNQUEUE_MCP_HTTP_SESSION_TTL_MS (30 minutes) but never during a running tool call, an unknown session gets 404, and request bodies are limited to 4 MiB.

If your application uses the workflow engine, an agent can see its runs and approve the ones waiting for a human signal: “approve expense run 4f1c…”. Point BUNQUEUE_MCP_WORKFLOW_DB at the Engine’s dataPath file and three tools appear:

ToolWhat it does
bunqueue_list_workflow_executionsLists runs, newest first. Filter by workflowName and state, page with limit and offset
bunqueue_get_workflow_executionShows one run: input, every step with its status and result, signals received, failure reason
bunqueue_signal_workflowDoes what engine.signal(executionId, event, payload) does in your app. The first signal for an event wins and cannot be undone

The MCP server never runs your steps: their code lives in your application. To resume a run, it records the signal and queues the next step for your Engine. That needs this setup:

  • Your Engine connects to a bunqueue server over TCP, with its own dataPath file: new Engine({ embedded: false, connection: { host, port }, dataPath: '/srv/app/data/workflows.db' }).
  • The MCP server runs in TCP mode against the same server, on the same machine, and can write that file.
{
"mcpServers": {
"bunqueue": {
"command": "bunx",
"args": ["--package=bunqueue", "bunqueue-mcp"],
"env": {
"BUNQUEUE_MODE": "tcp",
"BUNQUEUE_HOST": "your-server.com",
"BUNQUEUE_PORT": "6789",
"BUNQUEUE_TOKEN": "your-auth-token",
"BUNQUEUE_MCP_WORKFLOW_DB": "/srv/app/data/workflows.db"
}
}
}
}

Use an absolute path. The file must already exist: the MCP server never creates it.

Embedded apps signal from the app. An embedded Engine keeps its step queue inside your application, out of reach of any other process. The MCP server detects this: it still lists and shows runs, but leaves out bunqueue_signal_workflow and prints why on startup. Call engine.signal() in your app instead.

Limits and details
  • If you changed the Engine’s queueName, set BUNQUEUE_MCP_WORKFLOW_QUEUE to the same value (default __wf:steps).
  • A signal for a different event than the one a timed gate waits for is refused, so a typo cannot open a later gate. For gates without a timeout the awaited event is not recorded, so the run shows waitingFor: null.
  • The MCP server cannot check that it talks to the same bunqueue server and queue as your Engine. If it does not, the run shows running until your app calls engine.recover().
  • Your app’s signal:received listeners do not fire for signals sent through MCP; the step and workflow events that follow do.
  • SQLite over a network filesystem is not safe: keep the file local.
  • With BUNQUEUE_MCP_CONFIRM=destructive, every signal asks for confirmation first, with a preview of the payload.

The agent discovers the tools on its own; this table shows what is available. Every name starts with bunqueue_.

GroupToolsExamples
jobs12add_job, add_jobs_bulk, get_job, get_job_result, wait_for_job, get_job_logs
job-control7cancel_job, promote_job, change_job_priority, move_to_delayed, discard_job
processing8pull_job, ack_job, fail_job, job_heartbeat, extend_lock
queues11list_queues, get_job_counts, pause_queue, drain_queue, clean_queue
dlq5get_dlq, get_dlq_stats, retry_dlq, purge_dlq, retry_completed
cron4add_cron, list_crons, get_cron, delete_cron
limits5set_rate_limit, set_concurrency, get_queue_limits, clear_rate_limit
flows5add_flow, add_flow_chain, add_flow_bulk_then, get_flow, get_children_values
workers7register_worker, list_workers, register_handler, list_handlers
webhooks4add_webhook, list_webhooks, set_webhook_enabled, remove_webhook
monitoring7get_stats, get_queue_stats, get_memory_stats, get_prometheus_metrics
workflows (optional)3list_workflow_executions, get_workflow_execution, signal_workflow

Errors come back as plain messages, never stack traces. Inputs are checked by each tool’s schema before anything reaches the queue (for example: names cannot be empty, priorities are whole numbers within ±1,000,000, attempts go from 1 to 1000; these tool limits are stricter than the broker, which accepts any finite priority), and a rejected call changes nothing.

The server also offers 3 prompts, bunqueue_health_report, bunqueue_debug_queue and bunqueue_incident_response, and 5 read-only resources: bunqueue://stats, bunqueue://queues, bunqueue://crons, bunqueue://workers and bunqueue://webhooks.

Job options: retries, timeouts, custom ids, deduplication

add_job and each item of add_jobs_bulk accept priority, delay, attempts, backoff (a number, used as an exponential base, or { type: 'fixed' | 'exponential', delay, maxDelay }), timeout, jobId, deduplication ({ id, ttl, extend, replace }, per queue), removeOnComplete, removeOnFail, durable, lifo, tags and stallTimeout. Adding a jobId again while that job is unfinished returns the existing job. The flow tools take the same options, except deduplication, tags and durable. get_job shows the options a job carries.

Processing jobs by hand: lock tokens

Pass owner (and optionally lockTtl, from 1 second to 24 hours, default 30 seconds) to pull_job or pull_job_batch, and each job comes back locked with a token. Pass that token to ack_job, fail_job and job_heartbeat (tokens, one per job, for the batch tools; "" for a job pulled without an owner). A wrong or missing token is an error and the job stays active. A heartbeat with the token renews the lock; a lock that is not renewed expires and the job is retried. fail_job with unrecoverable: true sends the job straight to the dead letter queue.

Why jobs failed: the dead letter queue

get_dlq returns entries, oldest first, each with the job and why it failed: reason (explicit_fail, max_attempts_exceeded, timeout, stalled, ttl_expired, worker_lost or unknown), the last error, the last 10 attempts (attemptCount is the total) and its retry and expiry times. Filter by reason, and page with limit (1 to 100, default 20) and offset while hasMore is true. get_dlq_stats gives the total, the count per reason, the entries due for an automatic retry, the expired entries and the oldest and newest entry.

Limits and cron schedules

set_rate_limit takes duration, the window in milliseconds (default 1000), so “100 per minute” is limit: 100, duration: 60000. get_queue_limits reads back the rate limit, the concurrency limit, the active jobs, whether a limit is holding jobs back, and whether the queue is paused.

add_cron takes timezone (an IANA name such as Europe/Rome, together with schedule), jobName, priority, maxLimit (remove the schedule after that many runs), immediately, skipIfNoWorker, preventOverlap (on by default: at most one pending job per schedule), skipMissedOnRestart, deduplication, and options for the jobs it adds: attempts, backoff, timeout, delay, stallTimeout, removeOnComplete and removeOnFail. list_crons and get_cron show the time zone, job name, priority and run limit; the other settings are applied but not shown, because the server does not return them.

Set them in the env block of the server config. An invalid BUNQUEUE_MCP_* value stops the server at startup with a message that says what is accepted.

Connection

VariableDefaultDescription
BUNQUEUE_MODEembeddedembedded or tcp
BUNQUEUE_HOSTlocalhostYour bunqueue server (TCP mode); empty means localhost
BUNQUEUE_PORT6789Its TCP port, from 1 to 65535; any other value stops the server at startup
BUNQUEUE_TOKENnoneIts auth token
DATA_PATHnone (in memory)SQLite file for embedded mode (BUNQUEUE_DATA_PATH and BQ_DATA_PATH take priority)

HTTP server

VariableDefaultDescription
BUNQUEUE_MCP_TRANSPORTstdiostdio or http
BUNQUEUE_MCP_HTTP_HOST127.0.0.1Address to listen on; any other than loopback needs a token
BUNQUEUE_MCP_HTTP_PORT6791Port; 0 picks a free one
BUNQUEUE_MCP_HTTP_PATH/mcpEndpoint path
BUNQUEUE_MCP_HTTP_TOKENnoneComma-separated bearer tokens
BUNQUEUE_MCP_HTTP_MAX_SESSIONS100Sessions open at once
BUNQUEUE_MCP_HTTP_SESSION_TTL_MS1800000Idle time before a session closes
BUNQUEUE_MCP_HTTP_ALLOWED_HOSTSnoneExtra accepted Host names: name or name:port
BUNQUEUE_MCP_HTTP_ALLOWED_ORIGINSnoneExtra accepted browser origins, such as https://app.example.com

Agent features

VariableDefaultDescription
BUNQUEUE_MCP_TOOLSETSall toolsall, a list of groups, or dynamic (optionally followed by groups to preload)
BUNQUEUE_MCP_CONFIRMoffdestructive to require confirmation for the nine destructive tools (ten with workflow tools)
BUNQUEUE_MCP_DECISION_PROVIDERnonetypesafe, cloudflare or systemone
BUNQUEUE_MCP_DECISION_MODELjev-latest / clef-flashModel id; required for systemone
BUNQUEUE_MCP_DECISION_API_KEYnoneAPI key; required for typesafe and cloudflare
BUNQUEUE_MCP_DECISION_ACCOUNT_IDnoneCloudflare account id
BUNQUEUE_MCP_DECISION_URLprovider defaultEndpoint; required for systemone
BUNQUEUE_MCP_DECISION_TIMEOUT_MS10000Request timeout (retried once on network errors, 429 and overload)
BUNQUEUE_MCP_DECISION_THRESHOLD0.8How sure the model must be that you asked for a destructive operation

Workflow approvals

VariableDefaultDescription
BUNQUEUE_MCP_WORKFLOW_DBnoneThe Engine’s existing dataPath file; turns the workflow tools on
BUNQUEUE_MCP_WORKFLOW_QUEUE__wf:stepsThe Engine’s queueName, if you changed it

bunx bunqueue-mcp returns a 404. bunqueue-mcp is not its own npm package: it lives inside bunqueue. Use bunx --package=bunqueue bunqueue-mcp.

requires "@modelcontextprotocol/sdk". Since v2.8.1 the MCP SDK is an optional peer dependency, so queue-only users get a 94% smaller install and it is not installed with bunqueue. Run bun add -g @modelcontextprotocol/sdk (or bun add @modelcontextprotocol/sdk in your project).

The agent does not see the tools. Restart the client and check that the config JSON is valid. Then run bunx --package=bunqueue bunqueue-mcp in a terminal to see the actual error.

Jobs stay in waiting. Nothing is processing the queue. Register an HTTP handler or run a Worker.

HTTP mode: 401, 403 or 404. 401: the client sends no Authorization: Bearer <token> header, or a token that is not in BUNQUEUE_MCP_HTTP_TOKEN. 403 (invalid Host header or Origin): the request came through a name the server does not know, so add it to BUNQUEUE_MCP_HTTP_ALLOWED_HOSTS or _ALLOWED_ORIGINS. 404 Session not found: the server restarted or the session expired, so reconnect the client.

Refusing to serve MCP over HTTP on non-loopback host. You set BUNQUEUE_MCP_HTTP_HOST to a public address without BUNQUEUE_MCP_HTTP_TOKEN. Add a token, or listen on 127.0.0.1 behind a reverse proxy.

Lock token required for job <id>. The job was pulled with an owner, so it is locked. Pass the token from the pull to ack_job, fail_job or job_heartbeat. move_to_delayed, change_delay and discard_job cannot unlock it: finish it with fail_job or wait for the lock to expire.

Unknown time zone. Use an IANA name such as Europe/Rome or America/New_York, not an offset like +02:00. A timezone also needs a schedule.

wait_for_job returns Job not found. No job has that id.

BUNQUEUE_MCP_WORKFLOW_DB ... does not exist or is not a bunqueue workflow database. Use the absolute path of the dataPath file of an Engine that has started at least once. The same variable is missing if you see BUNQUEUE_MCP_TOOLSETS selects toolsets with no tool in this configuration: workflows.

bunqueue_signal_workflow is missing. The server printed the reason at startup: it runs in embedded mode, or the workflow file belongs to an embedded Engine. See workflow approvals.