Skip to content

Reject bad tool arguments instead of silently dropping them - #4

Merged
riccardone merged 1 commit into
mainfrom
mcp-strict-arg-validation-best
Aug 4, 2026
Merged

Reject bad tool arguments instead of silently dropping them#4
riccardone merged 1 commit into
mainfrom
mcp-strict-arg-validation-best

Conversation

@riccardone

Copy link
Copy Markdown
Contributor

Ports the bsp-mcp 1.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_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. data is 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 inputSchema before 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. The data-instead-of-params case gets an explicit hint.

Two deliberate choices:

  • Server-side, not additionalProperties: false on 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.
  • Underscore-prefixed keys are ignored, by the convention that they carry host/protocol metadata rather than tool arguments — ListTools already uses _meta itself.

It 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. 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:

case result
data instead of params validation error + hint, no network call
get_query_schema without version missing-argument error, not /undefined
execute_query without schema missing-argument error
valid args reaches fetch — not over-rejecting
poll_parameters alias reaches fetch — alias still works

The last two are the negative controls.

Release

No version bump in this PR — scripts/release-mcp.sh does that when cutting the release. Once merged, ./scripts/release-mcp.sh 2.0.1 publishes @behavioralstate/best-mcp@2.0.1.

Note the sibling release is already out: @behavioralstate/bsp-mcp@1.7.2 (tag mcp/v1.7.2, branch mcp-strict-arg-validation, cut from mcp/v1.7.1). That one could not use the release script — it requires main, and main is this BEST line, so running it there would have published best-mcp@1.7.2 and moved its latest backwards from 2.0.0. Worth correcting the README release table, which still maps mcp/v*@behavioralstate/bsp-mcp.

🤖 Generated with Claude Code

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>
@riccardone
riccardone merged commit cab305f into main Aug 4, 2026
6 checks passed
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.

1 participant