Skip to content

fix: enforce the declared bash and jq floors when the file is sourced - #22

Merged
Martin Bens (SpiGAndromeda) merged 1 commit into
mainfrom
fix/preflight-dependency-guards
Sep 13, 2026
Merged

Martin Bens (SpiGAndromeda) merged 1 commit into
mainfrom
fix/preflight-dependency-guards

Conversation

@SpiGAndromeda

@SpiGAndromeda Martin Bens (SpiGAndromeda) commented Sep 13, 2026 •

Copy link
Copy Markdown
Collaborator

lib/mcpserver_core.sh has declared bash 4.1+ and jq 1.7+ in its header line and in README.md §Requirements since 3.0.0. Nothing read either one.

What that cost

bash below 4.1 — the file still parses, so there was no early error. run_mcp_server died on its exec {_MCP_LIFELINE_FD}<> redirection with {_MCP_LIFELINE_FD}: not found and status 127, which an MCP host reports as nothing more than a server that failed to start. macOS ships 3.2.57 as /bin/bash, and both usual launch paths — a #!/usr/bin/env bash shebang, and a host manifest whose command is bash — resolve through PATH.

jq below 1.7 — nothing failed at all. The server answered every request while validate_tool_arguments returned the pre-3.0.0 verdict for a declared integer: reading a number back through tojson to catch a fraction the double rounded away depends on the literal preservation jq added in 1.7.

What the guard does

Sourcing checks bash, then jq's presence, then jq's version, and refuses with the requirement, the version found where there is one, and remediation for the platform — the package manager's command where one can be determined, a generic line otherwise.

The block sits above the file's own set -euo pipefail, so a refusal leaves the calling shell's options as it set them. It uses no construct newer than bash 3.2: a guard that cannot run on the version it rejects never gets the chance to speak. Diagnostics go to stderr — stdout carries the JSON-RPC stream, and no protocol error is constructible at that point anyway, since every envelope is built with jq -n.

A jq that is present but cannot run — wrong architecture, missing shared library — is named as such, with the path it resolved to, rather than reported as an outdated one.

For consumers

The floors are read at source time, so PATH has to select the intended jq and bash at that point. A server script that repaired PATH after sourcing moves that line above the source. An operator who cannot edit the script sets PATH in the host manifest's launch command, which also selects the bash the server runs under:

"command": "bash",
"args": ["-c", "export PATH=/opt/homebrew/bin:$PATH; exec /path/to/server.sh"]

Closes #21

The header line and README §Requirements have declared bash 4.1+ and jq 1.7+ since 3.0.0, and nothing read either one. Both failed in ways an operator could not diagnose from what reached them. Below bash 4.1 the file still parses, so the first sign of trouble was `run_mcp_server` dying on its `exec {_MCP_LIFELINE_FD}<>` redirection with `{_MCP_LIFELINE_FD}: not found` and status 127, which an MCP host reports as nothing more than a server that failed to start — and macOS ships 3.2.57 as /bin/bash, which both usual launch paths resolve through PATH. Below jq 1.7 nothing failed at all: the server answered every request while `validate_tool_arguments` returned the pre-3.0.0 verdict for a declared `integer`, because reading a number back through `tojson` to catch a fraction the double rounded away depends on the literal preservation jq added in 1.7.

Sourcing now checks bash, then jq's presence, then jq's version, and refuses with a message naming the requirement, the version found where there is one, and remediation for the platform. The block sits above the file's own `set -euo pipefail` so a refusal leaves the calling shell's options as it set them, and uses no construct newer than bash 3.2 — a guard that cannot run on the version it rejects never gets to speak. Diagnostics go to stderr: stdout carries the JSON-RPC stream, and no protocol error is constructible there, since every envelope is built with `jq -n`.

A jq that is present but cannot run is named as such, with the path it resolved to, rather than reported as outdated; `command -v jq` succeeds for a binary of the wrong architecture or with a missing library, and the advice to upgrade an already-current package sent the operator nowhere.

The floors are read at source time, so PATH has to select the intended jq and bash at that point. A server script that repaired PATH after sourcing moves that line above the `source`; an operator who cannot edit the script sets PATH in the host manifest's launch command, which also selects the bash the server runs under. README §Requirements carries both forms.

Co-Authored-By: Claude <noreply@anthropic.com>
@SpiGAndromeda
Martin Bens (SpiGAndromeda) merged commit f26fb76 into main Sep 13, 2026
3 checks passed
@SpiGAndromeda
Martin Bens (SpiGAndromeda) deleted the fix/preflight-dependency-guards branch September 13, 2026 17:48
Martin Bens (SpiGAndromeda) added a commit that referenced this pull request Sep 13, 2026
Classifies the dependency guards from #22 as major. A consumer running below either declared floor could source the file before; the guards now refuse to source it. A server script that repairs PATH after sourcing the file no longer starts.

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bash 4.1+ and jq 1.7+ are declared in a comment and enforced nowhere

1 participant