github-mcp 4.2.0: vendor bash-mcp-sdk v5.0.0, and run only the tools a server declares - #8
Merged
Merged
Conversation
Up from v3.0.0. The vendored copy is byte-identical to lib/mcpserver_core.sh
at that tag and .mcp-sdk.lock records the release and its hash.
What the two intervening majors change for this plugin:
v4.0.0 makes the server survive malformed input. A line carrying more than
one JSON document, a document that is not an object, a tools/call whose
params is not an object, and a request whose id is neither a string nor an
integer each used to stop the server with no response, leaving every later
request unanswered; each is now answered. It also handles
notifications/cancelled, so a cancelled call's process group is signalled
instead of running to completion, and it refuses to serve a configuration or
tools file it cannot read rather than reporting an empty one.
v5.0.0 enforces the bash 4.1+ and jq 1.7+ floors when the file is sourced.
Both were already documented requirements that nothing checked: below them a
server either died with {_MCP_LIFELINE_FD}: not found before speaking, or ran
while silently skipping part of the argument validation its schemas declare.
Co-Authored-By: Claude <noreply@anthropic.com>
The write server wrote its own empty tools list at startup when enable_write_server was not true, and removed it from an EXIT trap. Two things were wrong with that. The mktemp template was "tools-empty.XXXXXX.json". mktemp only substitutes a run of X characters at the end of the template, so the placeholder was taken literally and every server on the machine created, and later removed, the same fixed path. The cleanup never ran either. run_mcp_server installs its own EXIT trap and replaces any handler the consumer set, so the file outlived every server that created it. Under the vendored v3.0.0 that combination was survivable: a tools list that had been removed read as an empty one, which is what the disabled server wanted anyway. From v4.0.0 a tools list that cannot be read is answered -32603, so anything removing the file — another instance, or git clean -X, since the path was ignored — turned a disabled write server into one that failed every initialize and tools/list. A shipped mcp-server-gh/tools-empty.json removes the startup write, the shared path, and the dependence on a trap the protocol layer owns. The .gitignore entry that existed for the artifact goes with it; the log files it appeared to also cover are matched by the repository-root .gitignore. Co-Authored-By: Claude <noreply@anthropic.com>
A tools/call resolves to a shell function by name, so a server could run any tool_* function it had sourced, declared or not. api.sh, label.sh and project.sh are shared by both servers and each carries tools the other does not declare, which put label_add, label_remove, project_item_add, project_status_set and api — which takes any HTTP method — on the read server. That server is always active and needs no configuration, so enable_write_server gated what tools/list advertised and nothing else: all five ran against the authenticated gh CLI whether or not the write server was enabled. In the other direction the write server carried api_read, label_list, project_list and project_view. An undeclared tool has no schema, and argument validation treats a missing schema as nothing to check, so every one of them also ran unvalidated. That is the protocol layer this repository relies on to keep tool arguments away from the unguarded numeric comparisons in _gh_post_process, and without it a crafted max_lines reached bash arithmetic evaluation and executed a command inside the server process, which holds the user's gh credentials. _gh_unset_undeclared_tools removes every sourced tool function the running server's tools list does not name. It derives the permitted set from that list rather than from a hardcoded roster, so a tool added to a shared lib cannot open the gap again, and an empty tools list leaves nothing callable — which is what now makes a disabled write server inert rather than merely silent about what it can do. The new suite asserts the property for both servers in both states and computes the undeclared set at runtime. Four of its five tests fail against the previous code; the fifth is the control. BREAKING CHANGE: a tool call that reached a write tool through the read server, or any tool through a disabled write server, now answers "Tool not found". Every tool a server declares is unaffected. Co-Authored-By: Claude <noreply@anthropic.com>
repo_file and search_code with download_to redirected gh straight into the destination. A call that ended partway through left a truncated file at the path the caller asked for, with nothing to distinguish it from a complete one. Cancellation makes that reachable: from bash-mcp-sdk v4.0.0 a cancelled call has its tool process group signalled, where the command previously ran to completion no matter what the client did. Both sites now write to a .partial sibling and rename once gh has exited successfully, so the destination only ever holds a whole body. Two existing defects go with the change: - _gh_download_file redirected stderr into the file as well (2>&1) and left it in place on failure, so a failed download wrote the GitHub API's error text to disk as the file's contents. - repo_file removed the destination when a download failed, which deleted whatever was already at that path. A failure now leaves it untouched. A cancelled call can leave a .partial file behind. That is deliberate: it is visibly not the requested file, where a truncated destination is not. Co-Authored-By: Claude <noreply@anthropic.com>
Also states the dependency floors the servers now enforce. README listed bash 4.0+, which was never the protocol layer's requirement and is now two minor versions below it, and gave no jq version at all. SETUP gains a bash prerequisite and a jq version check. Both say what a Mac needs, because the shape of the failure is specific: macOS keeps bash 3.2 at /bin/bash and no install replaces it, so installing a newer bash is only half the job — its directory has to precede /usr/bin on the PATH the MCP host launches the server with, and a host started from the desktop reads no shell profile. plugin-setup's skill reference is a byte-identical copy of SETUP.md and is re-synced here. Co-Authored-By: Claude <noreply@anthropic.com>
…e run The assertion that a disabled write server writes no tools list of its own searched the server directory and required the result to be empty. The directory is shared by every run of the suite, and running an earlier revision there leaves a startup-written list behind, so the test then failed against code that is correct — for a file its own run never created. It now records the matching paths before and after and requires them to be equal, which answers the question the test is actually asking. The shipped tools-empty.json does not match the pattern. Still fails against the previous server-write.sh, along with the two dispatch assertions. Co-Authored-By: Claude <noreply@anthropic.com>
ShellCheck SC2013: iterating $(grep ...) with `for` splits on every character in IFS and glob-expands each field, which is wrong for a list of paths even where these particular names happen to be safe. CI lints .bats files alongside .sh and .bash, so this failed the ShellCheck step while a local run limited to shell scripts passed. Co-Authored-By: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Vendors
shared/mcpserver_core.shfrom shopwareLabs/bash-mcp-sdkv5.0.0, up fromv3.0.0, and fixes what bumping it surfaced. Releases the plugin as 4.2.0.A server ran tools it never declared
The largest change here, found while checking the bump against the open issues.
A
tools/callresolves to a shell function by name, so a server could run anytool_*function it had sourced, declared or not.api.sh,label.shandproject.share shared by both servers and each carries tools the other does not declare. That putlabel_add,label_remove,project_item_add,project_status_setandapi— which takes any HTTP method — on the read server, which is always active and needs no configuration.enable_write_servergated whattools/listadvertised and nothing else. All five ran against the authenticatedghCLI whether or not the write server was enabled.An undeclared tool also has no schema, and argument validation treats a missing schema as nothing to check, so each of them ran with unvalidated arguments. That validation is what #3 identifies as the only thing keeping tool arguments away from the unguarded numeric comparisons in
_gh_post_process; without it, a craftedmax_linesreaches bash arithmetic evaluation and executes a command inside the server process, which holds the user'sghcredentials._gh_unset_undeclared_toolsremoves every sourced tool function the running server's tools list does not name. It derives the permitted set from that list rather than a hardcoded roster, so a tool added to a shared lib cannot reopen the gap. An empty tools list leaves nothing callable, which is what now makes a disabled write server inert rather than merely quiet about what it can do.This closes the reachable half of #3. The issue stays open for its own fix: the guarantee still rests on 45 schema declarations that nothing downstream re-checks, and a future edit dropping a
typereopens it silently.Dependency floors are now enforced
v5.0.0checks bash 4.1+ and jq 1.7+ when the file is sourced, and refuses to start below either. Both were already the protocol layer's documented requirements and nothing checked them: below the bash floor a server died with{_MCP_LIFELINE_FD}: not foundbefore writing a byte of JSON-RPC, which a host reports as nothing more than a failure to start; below the jq floor it ran while silently skipping part of the argument validation its own schemas declare.This stops servers that start today. macOS keeps bash 3.2 at
/bin/bashand no install replaces it, so a Mac needs a newer bash ahead of/usr/binon thePATHthe MCP host launches the server with — and a host started from the desktop reads no shell profile, so that is not necessarily thePATHyour terminal has. A refusal names the requirement, the version found, and the install command for the platform.README listed bash 4.0+, which was never the requirement, and gave no jq version at all. Both files now carry the floors, and
SETUP.md's byte-identical copy underplugin-setupis re-synced.Also fixed
Downloads are written atomically.
repo_fileandsearch_codewithdownload_toredirectedghstraight into the destination, so a call ending partway left a truncated file indistinguishable from a complete one. Cancellation makes that reachable — fromv4.0.0a cancelled call has its tool process group signalled, where the command previously ran to completion regardless. Both sites write to a.partialsibling and rename onceghexits cleanly. Two older defects go with it:_gh_download_fileredirected stderr into the file as well, so a failed download wrote the API's error text to disk as the file's contents, andrepo_filedeleted whatever already sat at the destination when a download failed.The disabled write server no longer writes its own tools list. The
mktemptemplate placed its placeholder mid-name, somktemptook it literally and every server on the machine shared one fixed path; the cleanup ran from anEXITtrap thatrun_mcp_serverreplaces, so it never ran at all. Harmless underv3.0.0, where a removed tools list read as an empty one — but fromv4.0.0an unreadable tools list answers-32603, so anything removing the file turned a disabled write server into one that failed everyinitialize. A shippedmcp-server-gh/tools-empty.jsonreplaces it.What the bump brings on its own
tools/callwhoseparamsis not an object, and a request whoseidis neither a string nor an integer were each fatal with no response, leaving every later request unanswered. Each is now answered.notifications/cancelledwas ignored, so a cancelledrun_logsorsearch_coderan itsghcommand to completion. No plugin change was needed for this.