Reject bad tool arguments instead of silently dropping them - #4
Merged
Conversation
Ports the bsp-mcp 1.7.2 fix to the BEST line, which had the defect unchanged.
The tools declared `required`, but nothing enforced it. MCP hosts are not obliged to validate
arguments, and hosts that don't forwarded whatever the model produced — so a misplaced key was
silently dropped and failed in a way nobody could diagnose from the outside:
- execute_query given send_command's `data` instead of `params` ran the query with NO parameters.
An unfiltered query is usually one the caller may not run, so the endpoint answered "not
authorised" — and two separate debugging sessions went hunting a permissions problem that did not
exist. A genuine team admin concluded their API key lacked the role it plainly had.
- get_query_schema without `version` fetched /queries/{schema}/undefined and reported a missing
schema rather than a missing argument.
handleExecuteQuery's existing `parameters` alias was aimed at exactly this and its comment names the
failure mode, but an alias can only ever cover one more spelling — and `data` is a real field on a
sibling tool, so it is a plausible mistake rather than a typo.
Every tool call is now validated against that tool's own declared inputSchema before the handler runs:
unknown keys and missing required keys both produce an error naming the offending key and the accepted
ones, and stating that nothing was sent to the endpoint. The `data`-instead-of-`params` case gets an
explicit hint, since that is the one that costs hours.
Enforced server-side rather than by putting `additionalProperties: false` on the declared schemas: a
host that does not validate is precisely the case this has to survive, and a host that does would then
also reject argument keys the protocol may add later. Underscore-prefixed keys are ignored, by the
convention that they carry host/protocol metadata rather than tool arguments — and ListTools already
uses `_meta` itself.
Also declares `poll_parameters` on send_command_and_wait. The handler has always accepted it as an
alias of `poll_params` but the schema never listed it, so validating against the schema would have
started rejecting a call that works today.
Adds prepublishOnly: npm run build. dist/ is gitignored yet ships in the tarball, so the published
artefact depends on whatever was last built locally — one forgotten build would publish stale code.
Verified end to end by driving the built stdio server with raw JSON-RPC against a closed port, which
makes "nothing was sent" checkable: `data` and both missing-argument cases return validation errors
without a network call, while valid arguments and the poll_parameters alias get through to fetch.
No version bump here — scripts/release-mcp.sh does that as part of cutting the release.
Co-Authored-By: Claude Opus 5 <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.
Ports the
bsp-mcp1.7.2 fix to the BEST line, which had the defect unchanged. Same diff, same rationale.The bug
The tools declared
required, but nothing enforced it. MCP hosts are not obliged to validate arguments, and the ones that don't forward whatever the model produced — so a misplaced key was silently dropped, failing in a way nobody can diagnose from the outside:execute_querygivensend_command'sdatainstead ofparamsran the query with NO parameters. An unfiltered query is usually one the caller may not run, so the endpoint answered "not authorised" — and two separate debugging sessions went hunting a permissions problem that did not exist. A genuine team admin concluded their API key lacked the role it plainly had.get_query_schemawithoutversionfetched/queries/{schema}/undefinedand reported a missing schema rather than a missing argument.handleExecuteQuery's existingparametersalias was aimed at exactly this, and its comment names the failure mode — but an alias can only ever cover one more spelling.datais a real field on a sibling tool, so it's a plausible mistake rather than a typo.The fix
Every tool call is validated against that tool's own declared
inputSchemabefore the handler runs. Unknown keys and missing required keys both produce an error naming the offending key, the accepted ones, and stating that nothing was sent to the endpoint. Thedata-instead-of-paramscase gets an explicit hint.Two deliberate choices:
additionalProperties: falseon the declared schemas. A host that doesn't validate is precisely the case this has to survive; a host that does would then also reject argument keys the protocol may add later. Validating here works regardless of host behaviour, and lets the error name the offending key.ListToolsalready uses_metaitself.It also declares
poll_parametersonsend_command_and_wait. The handler has always accepted it as an alias ofpoll_params, but the schema never listed it — so validating against the schema would have started rejecting a call that works today. That was the one real regression risk in this change.And it adds
prepublishOnly: npm run build:dist/is gitignored yet ships in the tarball, so the published artefact depends on whatever was last built locally. One forgotten build would publish stale code.Verification
Drove the built stdio server with raw JSON-RPC against a closed port, which makes "nothing was sent" checkable:
datainstead ofparamsget_query_schemawithoutversion/undefinedexecute_querywithoutschemafetch— not over-rejectingpoll_parametersaliasfetch— alias still worksThe last two are the negative controls.
Release
No version bump in this PR —
scripts/release-mcp.shdoes that when cutting the release. Once merged,./scripts/release-mcp.sh 2.0.1publishes@behavioralstate/best-mcp@2.0.1.Note the sibling release is already out:
@behavioralstate/bsp-mcp@1.7.2(tagmcp/v1.7.2, branchmcp-strict-arg-validation, cut frommcp/v1.7.1). That one could not use the release script — it requiresmain, andmainis this BEST line, so running it there would have publishedbest-mcp@1.7.2and moved itslatestbackwards from 2.0.0. Worth correcting the README release table, which still mapsmcp/v*→@behavioralstate/bsp-mcp.🤖 Generated with Claude Code