gm-mcp
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., "@gm-mcprun git_status in the current project"
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.
gm-mcp
MCP server exposing gm's spool dispatch cycle (write, poll, cleaned response) to any MCP client.
What it does
Wraps the whole gm spool write-then-poll-for-response dispatch cycle into a single MCP tool call named gm, instead of a caller writing the input file, polling for the output file, and reading it as three separate steps.
Writes
.gm/exec-spool/in/<verb>/<N>.txtPolls
.gm/exec-spool/out/<verb>-<N>.jsonuntil it appears (or a timeout elapses)Returns the response as flat YAML text, auto-cleaned for readability:
opaque internal ids (
dispatch_id,request_fingerprint) strippedthe redundant
response/datanesting levels flattened to the top (unless a field name would collide)long text fields (e.g.
instruction's full phase prose) truncated with a pointer naming the on-disk file to read for the full text -- the plain-text-body verbs get a far larger budget so theirstdoutcomes back inline (see "Long text inline limits")hit-array ranking internals (
cos/recencyinrecall_hits/bm25_hits/vector_hits/commits) dropped,scoreretained as ranked evidencebyte-identical object rows repeated inside one array collapsed to the first copy
empty/null/empty-string fields removed at every level, except an empty result list (
edges,reachable,reached,callees,functions,matches,definitions,references), which stays as[]so "nothing found" reads as an answer rather than a missing field; and afalseon a flag whose only meaning is the absence of a problem (session_mismatch,instruction_unchanged,instruction_suppressible_by_asserting_hash,recall_embed_failed,should_residual_scan,fsm_graph_rejected)
Spool paths appear on timeout/abort/error or when compaction points to the original payload; other successful responses omit them (the caller already knows verb/cwd).
Supports plain-text-body verbs (
exec_jsand every language stem it backs, pluscrawl) via araw_bodystring parameter (a stringbody, or abodyobject with exactly one string field amongcode/script/command/source/text, is accepted as the same text), since these verbs reject a JSON-object body outrightAdds a
timeoutMs=<ms>first line to an exec-familyraw_bodythat has none, derived fromtimeout_seconds(see "Exec-family timeout prefix" below)
Related MCP server: agent-bus-mcp
Usage
Not published to npm -- gm-mcp is an unrelated package on the npm
registry. The shipped bin/gm-mcp-server.js is a pre-bundled, dependency-free
file (see Development below); npx github:AnEntrypoint/gm -g (the gm
installer) vendors it to ~/.gm-tools/gm-mcp-server.mjs and registers that
local file with every agent host:
{
"mcpServers": {
"gm": {
"command": "node",
"args": ["/home/you/.gm-tools/gm-mcp-server.mjs"]
}
}
}Never register npx -y github:AnEntrypoint/gm-mcp as the server command: npx
re-resolves the git ref over the network and reinstalls on every connect
(8.2s warm cache, 22.2s cold, vs 0.19s launching the bundle from disk), which
trips Claude Code's 30s connect timeout under load.
Self-updating deployed copy
A bundle running from ~/.gm-tools/gm-mcp-server.mjs (or $GM_TOOLS_DIR) checks
itself against bin/gm-mcp-server.js on main in the background after it
connects, at most once per hour. On a sha256 mismatch it validates the fresh
bundle (size, shebang, node --check), keeps the old one as
gm-mcp-server.mjs.prev, atomically swaps the new one in, and prints one
stderr line (deployed bundle was stale -- refreshed <old> -> <new> ...); the
refreshed tool schema is served from the next connect. A failed check keeps the
deployed copy and prints one line. Env: GM_MCP_SELF_UPDATE=0 disables,
GM_MCP_BUNDLE_URL (http(s) or file:) overrides the source,
GM_MCP_SELF_UPDATE_INTERVAL_MS overrides the throttle. A bundle from before
this feature cannot update itself: run npx github:AnEntrypoint/gm --mcp-only
once.
The release channel is a plain file swap, so a release that loses a fix would
silently undo it on the next check (exactly what happened to the
windowsHide: true on two child spawns in the 2026-10-04 08:57 build). Three
guards stand between the channel and the deployed file:
Freeze.
GM_MCP_NO_SELF_UPDATEset to anything other than0/false/no/off, or agm-mcp-server.no-self-updatefile in the install dir ($AGENTPLUG_HOME, else~/.agentplug), stops every self-update before any network call.Local-build pin.
gm-mcp-server.local-build.jsonin the same dir holds the sha256, path, version and ts of a bundle you want to keep: while the installed bundle's sha256 matches it, the channel may not overwrite it.gm-mcp pin-local-build [path]writes it,gm-mcp unpin-local-buildclears it, andgm-mcp self-update-statusprints the whole picture.No downgrade. The candidate's version (its
BUNDLE_VERSIONassignment) must be strictly greater than the installed one; equal or older is refused, and so is a candidate whose version cannot be read at all.
Every refusal goes to stderr with its reason (refusing deployed bundle self-update (<code>) -- <reason>), so it lands in the daemon log instead of
being swallowed by the background check. The .prev backup and the node --check validation of a candidate are unchanged.
Picking up a newer bundle without a reconnect
The self-update above compares the deployed file against a URL, so it answers
current whenever those two agree -- whatever bytes the running process
actually started from. A bundle installed over ~/.gm-tools/gm-mcp-server.mjs
by any other route (a lane build, dev-sync, a manual copy) therefore never
reaches a long-lived server through it, and a process runs the bytes it was
started from. Every 30 seconds the server instead compares the deployed file
against the file it launched from (launchedEntryPath()), size+mtime first and
sha256 only when that moved:
/healthanswers abundleblock (running_version,running_sha256,installed_version,installed_sha256,stale,reloadable,refusal), andgm-mcp http-statusprints it.Every dispatch reply carries one line naming both versions, so the mismatch is visible without asking.
fs_read,resultandgm_resultare the exception: their replies are file content handed back byte for byte, and a line appended there is not a note, it is a different file.
An HTTP server then replaces itself: it spawns the new bundle with an IPC
channel, sends it the listening socket, and stops accepting only once the
child answers gm-mcp-handover-ready. Both processes hold the socket through
the handover, so nothing is refused -- measured over 50s of continuous probing
across one handover: 42056 requests, 0 refused, one transition
0.2.34/36660 -> 0.2.35/32508. If the child never answers, the old
close-then-bind shape runs instead, and if the replacement never answers
/health the old process takes the port back.
A reload never cuts work. It does not start while any dispatch is in flight
(bundle-handover-deferred, retried on the next tick), and the outgoing
process waits for the in-flight count to reach 0 before exiting, so a reply
already being written is finished rather than truncated. Measured with a 40s
dispatch: the newer bundle was installed 4s in, /health went stale: true
while the pid stayed put, the dispatch came back complete at 42.3s carrying the
notice, and the server moved to the new bundle only afterwards.
Refusals report staleness and stay on the old bundle: no-downgrade,
installed-version-unknown and running-not-a-bundle (a dev checkout would
otherwise restart on every edit). A stdio server is not exempt -- it stays the
client's process and relays the client's frames to a child running the installed
bundle (gm-mcp: stdio handed to pid <n> on stderr), so the session is never
detached and no reconnect is needed; see AGENTS.md. Env: GM_MCP_BUNDLE_DRIFT_INTERVAL_SECONDS overrides the
30s watch, GM_MCP_BUNDLE_DRIFT_RELOAD=0 turns every reload off and keeps the
reporting.
Run bare in a terminal with no MCP client attached the server stays running
until stdin closes or the process is killed; a stdin close alone does not exit
the process (gm-mcp: stdin ended (client disconnected or platform pipe quirk) -- server stays up on stderr), since an MCP stdio client can legitimately
half-close stdin without ending the session. gm-mcp: connected, serving on stdio on stderr confirms the server started.
When the daemon is down
The daemon exits on purpose -- it self-recycles when idle or over its wasm
memory ceiling, and it hands off to a freshly downloaded runner build -- so a
dead daemon is normal, not a crash. This server restarts it on demand
(ensureSpoolRunnerRunning) and then on a timer per project root
(startRunnerWatchdog, every 5 s). Nothing else supervises it: there is no
systemd unit and no long-lived launcher to check.
Restart it by hand for one project with the same command the server uses --
the spool launcher detaches agentplug-runner daemon for that root:
On Windows:
cd C:/dev/mc-420
& "$HOME/.gm-tools/agentplug-runner.exe" spoolOn Unix:
cd ~/my/project
~/.gm-tools/agentplug-runner spoolA cold start compiles wasm for tens of seconds before it claims its first
ticket. To reinstall the runner entirely: npx github:AnEntrypoint/gm -g.
Logs, in the order worth reading:
~/.agentplug/daemon.log-- the daemon's own log (recycles, wasm compiles, plugin warnings). This is what the liveness notes mean by "daemon log".<project>/.gm/exec-spool/.watcher.log-- per-project spool events.<project>/.gm/exec-spool/.status.json-- the heartbeat:ts,pid,busy_until. Older than 20 s and itspidgone means no one is sweeping.<project>/.gm/exec-spool/in/<verb>/*.txt-- tickets nobody claimed.
A dispatch to a project whose daemon is gone does not wait out the poll
timeout: gmDispatch asks for a runner, waits DAEMON_START_GRACE_MS (15 s)
for the heartbeat to come back, and only then answers
error: daemon-not-running with the heartbeat age, both log paths and the
restart command -- and writes no ticket, so nothing is left queued behind a
daemon that will never claim it. A project with no .status.json at all is
the registration path rather than a dead daemon and still dispatches, as does
one mid-handoff (daemonBootGraceActive, runner_update_in_progress). Opt
out with GM_MCP_DAEMON_PREFLIGHT=0.
Transport: stdio (default) and streamable HTTP
gm is registered in ~/.claude.json as a user-scope MCP server:
{ "mcpServers": { "gm": { "command": "node", "args": ["C:\\Users\\user\\.gm-tools\\gm-mcp-server.mjs"] } } }That is a stdio server: the client owns the child process on a pipe. When
that one connection drops, the tool is gone for the rest of the session and
nothing can re-attach it -- the server cannot reconnect itself and the client
will not re-spawn it. It surfaces as No such tool available: mcp__gm__gm. Its MCP server 'gm' has disconnected, and the only cure is /mcp -> gm ->
Reconnect, or restarting the session.
Both transports ship in the same bundle, so the durable one is a one-line registration change:
claude mcp remove gm -s user && claude mcp add --transport http gm http://127.0.0.1:8787/mcp -s userThe HTTP side is stateless (no session id, one request per call), so a
client that disappears and comes back is just another request, and it binds
127.0.0.1 only. Manage it with:
node C:\Users\user\.gm-tools\gm-mcp-server.mjs ensure-http # start if nothing answers, arm the supervisor, print the url
node C:\Users\user\.gm-tools\gm-mcp-server.mjs http-status # is it answering
node C:\Users\user\.gm-tools\gm-mcp-server.mjs --http --port 8787 # run it in the foregroundEvery entry point seeds it: a stdio server, an HTTP server started on another
port and a dispatch run all health-probe the port and spawn the shared server
detached when nothing answers, so the durable transport is up before any client
asks for it and comes back by itself after a crash. GM_MCP_HTTP_SINGLETON=0
(or GM_MCP_NO_HTTP_SINGLETON=1) opts out, GM_MCP_HTTP_PORT moves the port.
Because no state is carried between requests, a restart of the HTTP server
never loses an in-flight dispatch.
The HTTP transport has its own stranding failure: if the port is empty when a
session's client connects, that client answers MCP server "gm" is not connected for the rest of the session, and no server-side fix re-dials it.
Restoring the port helps every new session; the stranded session needs /mcp
reconnect or a restart.
Across a reboot
Nothing starts the HTTP server at login, so after a reboot the registration points at a dead port until something runs it. ensure-http is that something, and it is idempotent: it health-probes the port, starts the shared server only when nothing answers, arms the supervisor, and prints the url either way. Put it wherever your OS runs things at login -- Startup folder, Task Scheduler, a launchd agent, a systemd user unit:
node ~/.gm-tools/gm-mcp-server.mjs ensure-http # Unix
node C:\Users\you\.gm-tools\gm-mcp-server.mjs ensure-http # WindowsThe supervisor is the part that has to survive, not the server. It is a detached http-supervise sibling that probes /health every 15 s (GM_MCP_HTTP_SUPERVISOR_INTERVAL_SECONDS moves it) and runs the same start path when the answer stops coming. ensure-http arms it on every run, including a run that found the server already up, so a supervisor that was killed comes back on the next ensure-http instead of leaving the durable transport unwatched. A --http server arms one for itself on start, and GM_MCP_HTTP_SUPERVISOR=0 opts out of both -- ensure-http then prints supervisor disabled.
Run the supervisor directly instead when you would rather supervise than probe once:
node ~/.gm-tools/gm-mcp-server.mjs http-supervise [--port N] [--interval S]It is the same loop in the foreground, so it is also what a service manager wants as its command. Two supervisors never share a port: a second one finds the first one's pid in the state file and exits with another supervisor owns this port.
State, both under ~/.agentplug ($AGENTPLUG_HOME moves it): gm-mcp-http.json holds the server's pid and url, gm-mcp-http-supervisor-<port>.json the supervisor's. The health probe is the authority, so a stale file naming a dead pid never blocks a fresh start.
Instances are registered, and the registry is swept
A --http server also registers itself as gm-mcp-http-instance-<port>.json
(pid, bundle version, entry path, supervisor pid). The port is the instance
identity, so a second GM_MCP_HTTP_PORT/--port is a legitimate second
instance and keeps its own supervisor; the registry is what stops instances from
piling up. ensure-http and every supervisor pass sweep it, and:
a port whose server no longer answers loses all three of its files, so that number is reusable instead of the next launch walking to a new one;
an instance serving a bundle older than the installed one is replaced in place -- the old server and its supervisor are stopped and a fresh server is started on the same port, so an attached session reconnects to current code instead of losing the port;
an instance with no live supervisor, a provably dead parent and zero established connections is stopped outright. A client still attached is never killed, and a parent that cannot be determined is left alone.
GM_MCP_HTTP_SUPERVISOR=0 still opts an instance out of supervision, and an
instance registered that way is never swept as an orphan.
Installing that boot hook from the repo
scripts/install-autostart.mjs writes the ensure-http call into the places this
repository cannot hold itself -- the login/boot hook and the user crontab -- and
then starts the server so the port is live immediately:
node scripts/install-autostart.mjs # install, refresh, and ensure one is listening
node scripts/install-autostart.mjs --check # show what would change
node scripts/install-autostart.mjs --uninstallIt is idempotent: every line it writes sits inside a marker block that a later
run replaces wholesale, and it never starts a second server (the ensure-http
health probe is the authority). --hook, --cron-file, --log and --entry
move the targets; --no-hook or --no-cron skips one. The crontab it installs
carries a @reboot entry and a five-minute entry, so a server that dies between
invocations is back within five minutes even when nothing else runs gm.
Development
bin/gm-mcp-server.js is a committed build artifact, not hand-edited source --
edit src/index.js/src/dispatch.js/src/cli.js instead, then rebuild:
Install development dependencies, then bundle src/cli.js into the committed
runtime artifact:
npm install
npm run buildRebuilding is not cosmetic: the bundle carries the tool's inputSchema, and
the MCP SDK silently STRIPS arguments the schema does not declare. A committed
bundle older than src/ therefore ships a server that drops a caller's new
argument without a word -- witnessed at commit d7f7cb6, whose src/ declared
resume_task while its bundle did not: {resume_task} with no body reached
gmDispatch as undefined, a fresh task was minted, a new spool entry was
written with an empty body, and the caller got the resumed verb's own
body-validation error (query required) with nothing pointing at the stale
bundle. Rebuild in the same commit as any src/ change.
npm run build and verify-build share scripts/build.mjs, whose version banner
comes from src/bundle-version.js and remains readable by the updater after
minification. verify-build rebuilds into memory, checks that production parser
against the source version, and fails on committed-bundle byte drift.
npm install points this checkout's git hooks at .githooks/
(core.hooksPath, local to this checkout, never committed) so pre-push runs
it automatically and blocks a push carrying a stale bundle.
npm test runs test/response-compact.test.mjs, which covers the payload
cleaner and the wire compactor in src/response-compact.js.
Bundling exists because npx github:... installs have been observed to
produce a corrupted transitive-dependency install (a node_modules/ajv
directory present but missing its package.json) on some npm/npx versions,
crashing the server before it can connect (CONNECTION_CLOSED on the client
side). Shipping a self-contained bundle with "dependencies": {} removes that
failure mode entirely -- there is nothing left for the installer to get wrong
at launch time.
Tool: gm
Parameter | Type | Required | Description |
| string | yes | gm spool verb name, e.g. |
| string | yes | gm |
| object | no | JSON body for the dispatch (not valid for plain-text-body verbs) |
| string | no | Literal text body for a plain-text-body verb, mutually exclusive with |
| string | no | Project root containing |
| number | no | Requested poll budget in seconds. Default 120, or an exec-family |
| number | no | Fallback response check interval when filesystem events are unavailable (default 0.25) |
| boolean | no | Include MCP submission-to-response timing and the last response wakeup source |
| string | no | The |
| boolean | no | Skip wire compaction and return every field verbatim, with no text field truncated (default: compacted) |
| number | no | Per-dispatch cap on how many characters of any one text field come back inline, overriding the defaults below (ceiling |
Verb tools
Every registry verb is also its own MCP tool. The tool name is the verb name with
each hyphen replaced by an underscore (prd-add becomes prd_add,
mutable-resolve becomes mutable_resolve; git_status and exec_js are
unchanged). The verb tool takes the same parameters as gm above except verb,
and forwards to the same dispatch path with that verb set. The registry list
lives in src/verbs.js (RUNNER_REGISTRY_VERBS). The generic gm tool stays registered.
Wire compaction
Every dispatch response is compacted before it crosses the wire; nothing is
removed from the out-file on disk. A compacted response carries a
wire_compacted block naming every field dropped or shortened, the on-disk
file holding the full payload, and the opt-out:
wire_compacted:
omitted: orient_nouns reply_hash route_hint supply_chain_scan
shortened: >-
codeinsight_overview codeinsight_start config_changed(1/2)
prd_items_truncated ready_wave(1/1) recall_hits(4/5)
full_payload_at: C:/proj/.gm/exec-spool/out/instruction-task-1.json
full_payload_via: 'dispatch with {"full_response": true}'Long prose is cut to a 160-char excerpt ending in ...+<n>, so an abbreviated
field always says how much is missing. full_response: true returns the
original guest fields and data layout, including dispatch_id and
request_fingerprint, without cleaning or compaction. MCP wait and resume
metadata remains separate.
phase_history retains the newest five transitions in its existing order. The
wire_compacted.shortened entry records retained/total counts and points to the
unchanged out-file. Current phase, gate decisions, session mismatch, PRD counts,
and failure payloads are not shortened. Use full_response: true for all history.
Committed git_commit and git_finalize successes retain at most five received
examples in excluded and excluded_but_dirty. Metadata records received-array
counts, not repository totals: five of 50 examples remains 5/50 even when
excluded_count is 193. Native totals, truncation counts, requested paths, commit
SHA and authors stay unchanged. The original out-file and full_response: true
recover all received examples; failures, refusals and uncommitted receipts bypass
this shortening.
Successful exhaustive output_mode: count replies omit output only when every
entry exactly repeats its corresponding structured counts row as path:count.
All count rows, totals and scan metadata remain unchanged. The original out-file
and full_response: true retain the repeated array; incomplete, failed and
nonmatching replies keep it.
Long text inline limits
Two env vars set how much of a long text field comes back inline before it is
replaced by the ... [N chars total, full text at <out-file> field '<key>']
pointer. They are read once at server start, so a host must restart its
gm-mcp-server.mjs for a change to take effect.
Env var | Applies to | Default | Ceiling |
| every long text field, including |
|
|
| the whole response of a plain-text-body verb ( |
|
|
| the file body |
|
|
| any value but | unset | -- |
The fs_read budget exists because that response is the file the caller
asked for: at the 400-char prose cap every whole-file read came back as a
pointer and the caller had to fall back to a host file-read tool. fs_read's
own max_bytes/offset/limit are daemon-side and were never the problem --
they were being cut down again on the way out.
A line range belongs to the fs_read request itself:
{"path":"src/dsp/audio_thread.cpp","startLine":1040,"endLine":1100} -- or
start/end, start/count, from/to, offset/limit, line/lines,
at the top level or inside body -- returns just those lines, 1-based and
inclusive on both ends, with total_lines, start_line, end_line and a
next_start_line to page on. limit/count/lines is a number of lines, not
an end line. A range-looking key that is not one of them is an error naming the
accepted keys, never a silent whole file, and a range that could not be applied
says so with range_applied: false. Without any range key the whole file comes
back, unchanged.
The plain-text-body budget exists because that response is the script's
output: truncating it at 400 chars meant nearly every exec_js call needed a
second round trip (a file read) to see its own stdout. Raise or lower either
knob in the mcpServers.gm entry:
{ "mcpServers": { "gm": {
"command": "node",
"args": ["/home/you/.gm-tools/gm-mcp-server.mjs"],
"env": { "GM_MCP_STDOUT_INLINE_MAX": "65536" }
} } }Non-numeric, zero or negative values fall back to the default. max_chars is
the per-dispatch override of all three: it is an MCP argument, so it never
reaches the verb's own body. full_response: true bypasses text caps, cleaning, data flattening and
wire compaction. The original response file remains subject to the bounded
4 MiB read limit.
Measure it against any real dispatch:
node scripts/measure-wire-size.mjs .gm/exec-spool/out/instruction-*.jsonExec-family timeout prefix
The daemon enforces timeoutMs=<ms> as a wall-clock limit on the exec child:
it defaults to 300000 when the line is absent, is clamped to a hard ceiling of
900000 (the reply then carries limit_clamped_from_ms), and at expiry the whole
process tree is killed and the reply is {ok:false, timed_out:true, killed:true, error_code:"exec_timeout", limit_ms, ...} with whatever stdout/stderr had been
produced. The exec family is exec_js (aliases nodejs, javascript, node, js) and every language stem: bash, sh, shell, zsh,
python, py, powershell, ps1, ssh, go, rust, c, cpp,
java, deno.
For these verbs the server adds the line itself when raw_body lacks one:
the value is the caller's own poll budget minus 5 s, floored at 300000 (
EXEC_DEFAULT_LIMIT_SECONDS, the daemon default) and capped at 900000 (agentplug-host'sHARD_CEILING_MS). A short poll therefore never shortens a child's life, and a long one buys the child the time the caller is waiting forwith no
timeout_secondsthe value stays 300000, the daemon defaulta
raw_bodythat already starts withtimeoutMs=<ms>ortimeout_ms=<ms>(leading whitespace allowed) is sent unchanged -- an explicit line wins
The prefix is the process budget the daemon enforces; timeout_seconds requests
the wrapper's poll budget and, for these verbs, raises that process budget to
match. Without an explicit poll value, an exec prefix requests its duration plus
5 s, with a 120 s minimum. Every initial or resume call is capped at 600 s, the
harness tool-call maximum -- set GM_MCP_POLL_TIMEOUT_CEILING_MS to move it.
A pending reply reports poll_timeout_ms, requested_poll_timeout_ms,
poll_timeout_capped, poll_timeout_ceiling_ms and poll_timeout_cap_reason
(a sentence naming why the wait ended, not just a boolean), and for exec verbs
exec_limit_ms plus an exec_child sentence stating that gm did NOT kill the
child and when the daemon will reap it. It also reports the original task and
spool paths. Resume that same task without a body; poll expiration neither stops
execution nor queues another dispatch. The daemon kills the child at timeoutMs,
so an abandoned call never leaves a runaway process -- it ends at that limit, and
the reply says so instead of leaving the caller believing the work continues.
An aborted call also withdraws its
request from the spool when the daemon has not claimed it yet
(request_withdrawn_before_claim).
Exec-family stdout, stderr and result are shown in full up to 16000
characters each. The out-file wraps them in a JSON string field data; the
wrapper unpacks it so the fields print flat. A longer field ends in
OUTPUT TRUNCATED: showing 16000 of N chars and names result_file, a plain
text <verb>-<task>.txt next to the out-file (sections ## result, ## stdout,
## stderr, un-escaped) that the daemon writes whenever a field exceeds 2000
characters; older daemons name the JSON out-file and its data field instead. Other long text fields stay capped at 400 characters with the same
kind of pointer.
raw_body is byte-for-byte -- mind the backslash escapes
The server writes raw_body to the spool input file with no escaping at all
(fs.writeFileSync(temp, body, 'utf8')). The shell therefore sees exactly the
bytes of the argument. Two escapes still apply before that point, and both
remove one backslash:
The MCP client encodes the tool argument as JSON. A backslash in a JSON string is an escape, so
\in your text reaches gm as\. Write\\to make the shell receive\.bashremoves one more backslash inside double quotes, and\$is a literal dollar sign. So\"C:\dir\${V}\"never expands${V}-- it is a quoted dollar, not an expansion.
A Windows path inside a bash double-quoted string therefore needs four backslashes per separator in the tool argument:
cmd /c "C:\\dev\\proj\\${B}.bat"Forward slashes avoid the problem entirely, and cmd.exe accepts them:
cmd /c "C:/dev/proj/${B}.bat"If a variable arrives at the shell literally (for example cmd /c reports
'C:\dev\proj${B}.bat' is not recognized), the cause is one of these two
escapes, not gm. Check the byte count of the backslashes before $.
Resuming a dispatch
A dispatch that outran its timeout_seconds is not lost: the daemon keeps
working and still writes its out-file. The timed_out response carries the
task that names it, plus dispatch_state (read from the spool itself:
claimed_still_in_flight / queued_not_yet_claimed / no_input_file_left) and
a project-scoped daemon liveness block.
Pass that task back as resume_task to re-poll the same dispatch:
a resume sends no body -- omit
body/raw_body; the dispatch being resumed already carries its own, and nothing new is written to the spoolverbandcwdmust match the original call exactly; together with the task name they are how the dispatch is addressed on disksession_idis still required by the tool for every call, though a resume writes no new spool file with ita triple matching no spool artifact returns an immediate error naming the three paths checked, rather than polling a task that cannot arrive
the response carries a
resumedblock stating that this call sent no body, wrote no new dispatch, and whether the result predates the resume -- so a stored error from the ORIGINAL dispatch is never misread as a verdict on the resume callresume_task_supported: trueon atimed_outresponse is the falsifiable signal that this server build honours the argument; older builds omit the field and silently drop it
This server cannot be deployed
Maintenance
Related MCP Connectors
Authenticated async Opus 5.5 agent with status polling and artifact results.
Hosted MCP messaging across owners, tools, and machines, with readable transcripts.
Remote MCP for 9192 discovery, pricebook, payment rails, GET_PULSE quotes, and receipt checks.
Remote MCP for Gemini upgrade evals, prompt regressions, output diffs, and eval receipts.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables running and managing automated tasks with retry loops and machine-checkable success criteria via MCP tools.0MIT
- FlicenseNot gradedqualityCmaintenanceMCP wrapper for the agent-msg.sh file-based message bus, enabling MCP-compatible clients to send, receive, and manage inter-agent messages with bound identity and HMAC-signed payloads.-
- AlicenseNot gradedqualityCmaintenanceEnables MCP clients to asynchronously submit tasks to a persistent coding agent already running on a development machine, disconnect, and retrieve results later through a secure, policy-controlled gateway.MIT
- AlicenseAqualityCmaintenanceEnables MCP clients to submit tasks to a local file-backed queue and later read or cancel them by ID, with results produced by a separately run worker.3MIT