gm-mcp
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
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 (gm_codemode, with gm.<verb>({...}) calls in its code), 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 id
request_fingerprintstripped;dispatch_idkept as the reply's actionable evidence (prd-list,prd-resolve'switness_dispatch_idandrs-plugkitprd.rsread it from the witness body, andcomplete.mdserves that contract to workers)the 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, plusbrowser) 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 Relay
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). The one-line install in the gm README (Install)
fetches it to ~/.gm-tools/gm-mcp-server.mjs and registers that absolute path.
Running the bundle with register does the same by hand on any OS:
node ~/.gm-tools/gm-mcp-server.mjs register. It is idempotent and rewrites a
broken gm entry, such as one carrying an unexpanded $HOME or ~. Any other
agent host takes the same stdio command:
{
"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
Any running copy checks the deployed ~/.gm-tools/gm-mcp-server.mjs (or
$GM_TOOLS_DIR) against bin/gm-mcp-server.js on main in the background
after it connects, at most once per hour, not only a process started from that
file, so a server launched from a repo build keeps the deployment current
instead of leaving it behind upstream. A process that refreshed a deployment it
is not does not hand its own transport over to it; only the deployed copy's own
process reloads into what it installed. 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 (see gm-mcp/AGENTS.md "Bundle drift"). A failed check keeps the
deployed copy and prints one line. Env: GM_MCP_SELF_UPDATE=0 disables,
GM_MCP_SELF_UPDATE_DEPLOYED_ONLY set to anything but 0/false/no/off
limits refreshing to the deployed copy's own process,
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: rerun the one-line install from the gm README
once (it fetches the current bundle).
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.
The check runs every GM_MCP_SELF_UPDATE_INTERVAL_MS (default 60 seconds). It
probes the GitHub API for the main head: a 304 with no body while the head is
unchanged, so an idle check moves no bundle bytes. A changed head is fetched by
its commit-addressed raw URL, and a fix pushed upstream reaches this machine
within one interval. gm-mcp self-update-now runs the same check at once, skipping only that stamp: it
starts no server, binds no port, signals nothing and never hands this process
over to the bundle it installs, and every guard above still stands.
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.
The runner and guest swap of the release bridge (src/release-bridge.js) is
serialised by the lock file gm-mcp-release-bridge.lock in the same dir. A
contended acquire is not queued: it makes two attempts with no wait and returns
{ outcome: 'busy' }. That is intended backpressure. The only caller is the
background tick, which treats busy as a silent no-op, and the holder is
already running the check that the next tick would run. A holder whose pid is
dead, or whose lock is older than 15 minutes, is treated as stale and taken over.
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 starts
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_readandgm_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 started by hand with --http does not replace itself. It keeps
serving the bundle it loaded, reports the mismatch on /health and in each
dispatch reply, and picks up the installed bundle only when a human restarts it.
Nothing spawns the newer bundle, hands it the listening socket or takes its port.
A bundle change takes effect when the next session starts. A stdio server that
finds a newer installed bundle keeps serving the bundle it loaded and says so once
(bundle-stale-no-handover in the server log, and one stderr line). Nothing spawns
the newer bundle or relays to it, so a stdio session adds no process per deploy.
Env: GM_MCP_BUNDLE_DRIFT_INTERVAL_SECONDS overrides the 30s watch.
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, rerun the one-line install from the gm README (install.sh or install.ps1).
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.
Continuation channel
Every dispatch reply ends with a continuation block: state (running or stopped), pending_rows (from the reply's prd_pending_count, null until one is seen), idle_ms since the previous reply on that session, and action. action is continue while rows are pending (with max_concurrency, the pending count capped at 4 workers), stop when no rows are pending or the session is stopped, and none when the pending count is unknown. A JSON object reply gets continuation as a field, a JSON array reply carries none, and file-content replies (fs_read, result, gm_result) stay byte for byte.
continuation-stop (dispatched through gm with verb: "continuation-stop", or on the session body) marks the session stopped. The next instruction dispatch with a non-empty prompt resumes it.
The first successful reply of a session that names a cwd also carries a bootstrap block: server (version and transport), runner (alive, heartbeat_age_ms, heartbeat_stale from the status verb), pending (count and the first pending row as next), checklist_uri and bootstrap_complete. Its status and next-row reads share one 8000 ms budget (BOOTSTRAP_BUDGET_MS, src/live-state.js), and the reply waits at most that long for them. A read that misses it, or fails, is recorded in bootstrap_error. Refused and error replies, and JSON array replies, leave the bootstrap for a later reply. The skill prompts and gm-completion take an optional cwd argument that adds the same live state as text.
Push is stdio only, and the server declares logging. When pending rows exist and no reply has arrived for GM_MCP_IDLE_NOTIFY_MS (default 300000), a notifications/message at level warning with logger gm.continuation carries the same object as data, once per idle period. A standalone stdio server emits it through its SDK server. A stdio session that relays to a server started by hand emits from the relay, which observes the continuation blocks in the replies it forwards. Streamable HTTP cannot push, so HTTP clients read continuation from the reply.
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, and stdio is the default: no flag selects it, and
the server opens no listening port. 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,
then gm, then Reconnect, or restarting the session.
A stdio start serves the session in-process over the spool: main in
src/index.js runs createServer on stdio, and each dispatch is published to
.gm/exec-spool by dispatch.js, so no port and no admin rights are needed on
Windows, macOS or Linux. The stdio path starts no HTTP server, singleton,
supervisor, instance registry or boot entry.
The streamable HTTP transport runs only when a human starts it by hand with
--http on the command line. --port N and --host H are optional (defaults
8787 and 127.0.0.1). No environment variable or default enables it, and nothing
starts it for you:
node ~/.gm-tools/gm-mcp-server.mjs --http --port 8787 # foreground; serves http://127.0.0.1:8787/mcp until stoppedThe 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. To register a server you started, run:
claude mcp remove gm -s user && claude mcp add --transport http gm http://127.0.0.1:8787/mcp -s usergm-mcp-server.mjs is the deployed copy under ~/.gm-tools (the tree ships
gm-mcp/bin/gm-mcp-server.js). http-status reports whether a server answers
on the default port and starts nothing.
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.
A stdio server cannot load a newer bundle in place, and it never exits, because the host treats its exit as a disconnect. A stale stdio server keeps serving the bundle it loaded until its session ends; the next session starts from the installed bundle. node ~/.gm-tools/gm-mcp-server.mjs self-update-status lists a stale process under running_images with stale: true until then.
Self-install on a fresh machine
On start the bundle installs the runner and guest a host needs, with no installer. The gm skill is not copied into any host skill root: it is served over MCP from the bundle (gm_skill, src/skill-verb.js), so no ~/.claude/skills or ~/.agents/skills file is written. The agentplug runner is installed from the AnEntrypoint/agentplug-bin release when ~/.gm-tools/agentplug-runner is absent, and the gm guest from AnEntrypoint/plugkit-bin, each checked against its sha256 and a self-check before it is renamed into place (src/release-bridge.js).
No supervisor, no boot entry
gm starts no supervisor, login or boot hook, crontab entry or service, and no
install script writes one. A server started with --http runs until that
process exits, and nothing brings it back after a reboot or a crash. If HTTP
must survive a reboot, define that with your own service manager and have it run
gm-mcp-server.mjs --http; gm does not write that definition.
Leak watch: gm defends itself at runtime
No CI job can see what is running on a developer machine, so the runtime is the
only place a process leak can be caught. Every server runs one leak pass on the
hourly pass of the scheduler it already had (scheduleStaleDeployedBundleChecks) and once at
startup, sharing a single host process-table read across the census, the reaper's
unattributed pass and the duplicate-supervisor count:
gm-mcp-server.js leak-watch # the same pass, by hand
gm-mcp-server.js leak-watch --json # the whole report
gm-mcp-server.js reap --apply # retire what the pass reportsA pass that finds nothing prints nothing. A pass that finds something writes a
leak-watch diagnostic and one stderr line naming the counts:
gm-mcp: leak watch -- 27 gm-mcp process(es) on this host (warn at 8); 19 server(s), 8 supervisor(s); 27 unattributed, 24 retireable; 2 duplicate supervisor(s) -- ABOVE the population warning thresholdThe population warning is the early signal. gm_processes counts every
gm-mcp-server process alive on the host, servers and supervisors, and
above_warn trips at GM_MCP_LEAK_POPULATION_WARN (default 8), long before
the count becomes memory pressure. It is a warning, never a kill: nothing is
refused a start.
Retiring is on by default and narrower than the reaper's own retire verdict. The
pass signals only what the rules below allow (GM_MCP_LEAK_AUTO_APPLY=0 turns it off),
limited to an unattributed stdio server that is also:
not the pid answering
/healthand not the owner of a registered port's listening socket, one shared HTTP server carries every session on the box;free of any live MCP-client ancestor (
claude,code,cursor, ...), so a server a session is driving is never touched;free of any live desktop-session ancestor (
selkies,xfdesktop,xfce4-session,s6-svscan,Xvfb, ...), those look exactly like multi-day orphans and are the live session;of a measured start time at least 30 minutes old, and at most 3 per pass.
A process whose ancestry or age cannot be read is never signalled: an unmeasured
value is never read as "old". Duplicate http-supervise --port <N> supervisors
are counted and reported; retiring them stays with the supervisor sweep
(GM_MCP_HTTP_SUPERVISOR_DUPE_REAP).
Orphans are the leaks the ledger cannot see. The reaper assesses only pids
the child ledger knows, so a process gm never spawned piles up invisibly, 11
reparented runners held 3367 MB of a 7.8 GB box on 2026-10-15 without one line
from the watch. The same pass now also scans for processes whose ppid is 1
(reparented to init), that no ledger row claims, that are heavyweight (RSS at
or above GM_MCP_LEAK_ORPHAN_RSS_MB, default 150 MB) and idle, CPU seconds
burned divided by seconds alive below GM_MCP_LEAK_ORPHAN_DUTY (default 0.02), after GM_MCP_LEAK_ORPHAN_MIN_AGE_MS (default 900000 ms, 15 minutes) alive.
The duty ratio is the discriminator: the 11 real leaks sat near 0.01 while the
one live run of the same binary is still burning CPU.
ORPHAN pid 1860399 -- 292.1 MB RSS, 791s alive, 15.5s cpu (duty 0.0196) -- target/debug/litebox_runner_linux_userland -Z --uid 0 ...An orphan is reported, never signalled: gm did not spawn it, so gm does not
classify whether it may be killed. Detection reads /proc/<pid>/stat and
/proc/<pid>/status directly, a ps on a box loaded enough to grow orphans
can take over 120 s and time out, which is exactly the box this scan exists to
diagnose, so on mac and Windows, which have no /proc, the section is simply
empty rather than wrongly quiet about a number it never measured.
Env var | Effect | Default |
|
| unset (on) |
| process count at which the pass goes loud |
|
|
| unset (on) |
|
| unset (on) |
|
| unset (on) |
| RSS floor at which a reparented process is heavyweight |
|
| CPU-seconds-per-second-alive below which it counts as idle |
|
| age floor, so a starting process is never reported |
|
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 is minted, a new spool entry is
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.
scripts/publish-api.mjs lands files on main through the GitHub Git Data API (blobs, a tree on the remote tip, a commit parented on it, then PATCH refs/heads/main). When no push run appears for the new sha within 60 s, it dispatches each active workflow_dispatch workflow on that sha, so a docs-only commit still gets CI. Usage: node scripts/publish-api.mjs <owner>/<repo> "<message>" <repoPath>=<localFile>....
scripts/prune-scratch.mjs --root <dir> [--cap-hours 24] [--apply] removes the directories directly under <dir> whose name starts with a gm-owned prefix (gm-agentplug-home-) and whose mtime is older than the cap. It is a dry run unless --apply is given, and --apply removes exactly the directories the dry run listed.
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
The server registers one MCP tool, gm_codemode. Every registry verb is a function
on gm inside its code body. Discover names with gm.list() and a verb's schema
with gm.describe(name). Call a verb as gm.<verb>({...}) or gm.gm({verb, body});
both take the same dispatch path. The registry list lives in src/verbs.js
(RUNNER_REGISTRY_VERBS). A client holding a cached tools/list from an earlier
build must re-fetch it.
A code body runs under a deadline of GM_CODEMODE_TIMEOUT_MS milliseconds (default
600000; a non-positive or non-numeric value falls back to the default). A body that
awaits a gm call that never returns, or a loop of calls that outlives the budget, fails
with gm_codemode code did not finish within <ms> ms; calls already dispatched keep running
in the spool and can be resumed with resume_task. The deadline cannot interrupt a
synchronous loop in the body (while (true) {}): the body runs on the server's single
thread, so that loop blocks every request until the process is killed. A body must bound
its own synchronous work.
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 | -- |
| 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/dispatch.js","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
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
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.14 npmMIT
- 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
- AlicenseNot gradedqualityBmaintenanceEnables running authorized file operations, persistent processes, and parallel batches from an MCP client, with durable receipts and artifact verification, and allows reconnecting to collect workers without restarting them.MIT