Skip to content

fix(mcp-bridge): harden argument handling for routed tools - #349

Merged
hyperpolymath merged 1 commit into
mainfrom
fix/bridge-arg-hardening
Oct 7, 2026
Merged

hyperpolymath merged 1 commit into
mainfrom
fix/bridge-arg-hardening

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

Summary

Harden argument handling in the MCP bridge for the routed tools: browser, cloud, comms and ml. These tools share a cartridge and are told apart by a routing key derived from the tool name. After this change the tool name alone decides the route, and arguments that a routed tool's inputSchema does not declare are refused with -32602.

📌 New pins

Head SHA: f01e55a. This PR adds or changes no action, lockfile or container pins.

Changes

  • mcp-bridge/lib/dispatcher.js:
    • New ROUTED_TOOLS table (tool name → cartridge plus routing key). It replaces four switch arms. The routing key is written after the caller's arguments.
    • New validateRoutedArgs and declaredArgs. The hardening gate refuses arguments that a routed tool's inputSchema does not declare.
    • The gate also refuses non-object arguments.
    • JSDoc added to dispatchTool, hardeningGate and the new helpers.
  • Scope: non-routed tools are unchanged. coord_send reads sender_role, which no schema declares, so applying the check to every tool would break it.
  • mcp-bridge/tests/routing_args_test.js (new, 16 tests). Each routed tool still reaches its cartridge with the right key. An argument cannot change the routing key. Undeclared arguments are refused. Non-routed handling is unchanged. fetch is stubbed for this file only and restored afterwards, because bun shares one process across files.
  • .github/workflows/e2e.yml (node, deno and bun unit lines) and package.json test now include the new file.

RSR Quality Checklist

Required

  • Tests pass. node --test: 68/68 across the four bridge test files. bun test: 68/68. deno test: 68/68.
  • Code is formatted: no formatter is configured for mcp-bridge/ JS. The code follows the surrounding style by hand.
  • Linter is clean: no JS linter runs on mcp-bridge/ locally. CI scanners will report on this PR.
  • No banned language patterns. Plain ESM JS, as in the existing bridge. Nothing new in TS, Python or Go. The npm test script line already existed and only gained a file name.
  • No unsafe blocks: there is no Rust or Zig in this change.
  • No banned functions.
  • SPDX headers: the new test file carries MPL-2.0. The modified files keep theirs.
  • No secrets, credentials or .env files.

As Applicable

  • .machine_readable/* not updated: project state and integrations are unchanged.
  • Documentation not updated: the advertised tool schemas are unchanged. Only calls that already violated those schemas are now refused.
  • TOPOLOGY.md not updated: architecture is unchanged.
  • CHANGELOG / release notes: to follow with the patch release that ships this fix.
  • New dependencies: none.
  • ABI/FFI: not touched.

Testing

  • node --test mcp-bridge/tests/{routing_args,dispatch,http_transport,path_claims}_test.js: 68 pass, 0 fail.
  • bun test (same files): 68 pass, 0 fail. deno test --allow-read --allow-env --allow-run --allow-net (same files): 68 passed.
  • Control: routing_args_test.js against origin/main's dispatcher.js gives 8 pass and 8 fail. Every routing and undeclared-argument test fails there; the well-formed-call and non-routed tests pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_019j8She9eTFx54r6aL6sCHP

The browser / cloud / comms / ml tools share a cartridge and are told
apart by a routing key derived from the tool name. Route them from one
table (ROUTED_TOOLS), write the routing key after the caller's
arguments, and refuse arguments their inputSchema does not declare.
Non-routed tools keep their existing argument handling.

Adds mcp-bridge/tests/routing_args_test.js (node, deno and bun) and
runs it in e2e.yml and `npm test`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019j8She9eTFx54r6aL6sCHP
@coderabbitai

coderabbitai Bot commented Oct 7, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 40 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 77350813-43f6-4f31-b854-db5b615720fe
📥 Commits

Reviewing files that changed from the base of the PR and between 9180ee6 and f01e55a.

📒 Files selected for processing (4)
  • .github/workflows/e2e.yml
  • mcp-bridge/lib/dispatcher.js
  • mcp-bridge/tests/routing_args_test.js
  • package.json
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown

🏁 path-claims bench

Commit 3b8f564

Numbers
path-claims bench  (node v22.23.3)

  scenario                                              iters       ms        ns/op          ops/s
  --------------------------------------------------------------------------------------------------------------
  register: 10 active claims, 3 new paths               50000 iters    181 ms      3.63 µs/op    275.6k ops/s
  register: 100 active claims, 3 new paths              20000 iters    319 ms     15.95 µs/op     62.7k ops/s
  register: 1000 active claims, 3 new paths              5000 iters    973 ms    194.72 µs/op      5.1k ops/s
  register: 100 active claims, 20 new paths              5000 iters    371 ms     74.37 µs/op     13.4k ops/s

  pathsOverlap: deep diverge at segment 4             1000000 iters    155 ms     155.6 ns/op     6.43M ops/s
  pathsOverlap: short prefix match                    1000000 iters    146 ms     146.5 ns/op     6.83M ops/s

  refresh (existing claim)                             100000 iters     11 ms     115.1 ns/op     8.69M ops/s
  list (100 active claims)                              50000 iters    296 ms      5.94 µs/op    168.4k ops/s

  (Bench numbers depend on host; use deltas across commits, not absolute values.)

Host-dependent — compare deltas across commits, not absolute values.

@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown

🔍 Hypatia Security Scan

Findings: 113 issues detected

Severity Count
🔴 Critical 10
🟠 High 20
🟡 Medium 83

⚠️ Action Required: Critical security issues found!

View findings
[
  {
    "reason": "Job `sonarqube` in build.yml has no `timeout-minutes:` declaration. Default is 6 hours — a stuck codeload fetch or runner hang can burn budget. Add `timeout-minutes: 10` (or proportional).",
    "type": "missing_timeout_minutes",
    "file": ".github/workflows/build.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium",
    "recipe_id": "recipe-add-workflow-timeout-minutes",
    "job": "sonarqube"
  },
  {
    "reason": "Job `triage` in label-triage.yml has no `timeout-minutes:` declaration. Default is 6 hours — a stuck codeload fetch or runner hang can burn budget. Add `timeout-minutes: 10` (or proportional).",
    "type": "missing_timeout_minutes",
    "file": ".github/workflows/label-triage.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium",
    "recipe_id": "recipe-add-workflow-timeout-minutes",
    "job": "triage"
  },
  {
    "reason": "Job `sync` in labels.yml has no `timeout-minutes:` declaration. Default is 6 hours — a stuck codeload fetch or runner hang can burn budget. Add `timeout-minutes: 10` (or proportional).",
    "type": "missing_timeout_minutes",
    "file": ".github/workflows/labels.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium",
    "recipe_id": "recipe-add-workflow-timeout-minutes",
    "job": "sync"
  },
  {
    "reason": "Job `deploy` in pages-deploy.yml has no `timeout-minutes:` declaration. Default is 6 hours — a stuck codeload fetch or runner hang can burn budget. Add `timeout-minutes: 10` (or proportional).",
    "type": "missing_timeout_minutes",
    "file": ".github/workflows/pages-deploy.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium",
    "recipe_id": "recipe-add-workflow-timeout-minutes",
    "job": "deploy"
  },
  {
    "reason": "Step uses `peter-evans/repository-dispatch` with `token: ${{ secrets.FARM_DISPATCH_TOKEN }}` but has no `if: secrets.FARM_DISPATCH_TOKEN != ''` gate. On repos where the secret hasn't been propagated the action fails on every push, red-maining the repo. Add the step-level gate (or env+if pattern) so the missing-secret path is a clean skip instead of a red.",
    "type": "secret_action_without_presence_gate",
    "file": ".github/workflows/instant-sync.yml",
    "action": "peter-evans/repository-dispatch",
    "rule_module": "workflow_audit",
    "severity": "high",
    "fix_recipe": "add_secret_presence_gate"
  },
  {
    "reason": "codeql.yml does not list `language: actions` in its matrix, but the repo has workflow files. CodeQL's `actions` language scans workflow YAML for injection and other CI/CD-specific weaknesses — every repo with workflows benefits. Add an entry to `matrix.include` with `language: actions` + `build-mode: none`.",
    "type": "codeql_missing_actions_language",
    "file": ".github/workflows/codeql.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium",
    "fix_recipe": "add_codeql_actions_language"
  },
  {
    "line": 39,
    "reason": "job in .github/workflows/labels.yml references `secrets.*` but does not install `step-security/harden-runner` — review outbound-egress monitoring",
    "type": "RE001",
    "file": ".github/workflows/labels.yml",
    "action": "report",
    "rule_module": "research_extensions",
    "severity": "medium"
  },
  {
    "line": 46,
    "reason": "job in .github/workflows/push-email-notify.yml references `secrets.*` but does not install `step-security/harden-runner` — review outbound-egress monitoring",
    "type": "RE001",
    "file": ".github/workflows/push-email-notify.yml",
    "action": "report",
    "rule_module": "research_extensions",
    "severity": "medium"
  },
  {
    "line": 32,
    "reason": "job in .github/workflows/build.yml references `secrets.*` but does not install `step-security/harden-runner` — review outbound-egress monitoring",
    "type": "RE001",
    "file": ".github/workflows/build.yml",
    "action": "report",
    "rule_module": "research_extensions",
    "severity": "medium"
  },
  {
    "line": 44,
    "reason": "job in .github/workflows/container-publish.yml references `secrets.*` but does not install `step-security/harden-runner` — review outbound-egress monitoring",
    "type": "RE001",
    "file": ".github/workflows/container-publish.yml",
    "action": "report",
    "rule_module": "research_extensions",
    "severity": "medium"
  }
]

Powered by Hypatia Neurosymbolic CI/CD Intelligence

@hyperpolymath
hyperpolymath merged commit 0e700c9 into main Oct 7, 2026
57 of 62 checks passed
@hyperpolymath
hyperpolymath deleted the fix/bridge-arg-hardening branch October 7, 2026 07:15
hyperpolymath added a commit that referenced this pull request Oct 8, 2026
## Summary

Every MCP tool's `inputSchema` declares `additionalProperties: false`,
but the bridge enforced that only for the 13 routed tools
(browser/cloud/comms/ml, since #349). This PR enforces it for every tool
in the full list, so a call carrying an argument its schema does not
declare is refused with JSON-RPC `-32602` before dispatch.

## 📌 New pins

Head SHA: **`f3b48f2b1d7c18f9f8f85a6db3312fd59b761d78`**. No action,
lockfile or container pins added or changed.

## Changes

- `mcp-bridge/lib/dispatcher.js`: `validateRoutedArgs` becomes
`validateDeclaredArgs` and applies to every tool. The routing-key
refusal for routed tools is kept. The deprecated
`coord_promote_to_supervisor` alias is checked against
`coord_promote_to_master`'s schema, and a tool missing from the full
list is refused as `-32601 Unknown tool` in the gate.
- `mcp-bridge/lib/tools.js`: declares arguments the handlers already
read.
- `sender_role` (optional enum) on `coord_send` and `coord_send_gated`.
- `role` and `capabilities` on `coord_register`, copied from the
local-coord-mcp `cartridge.json`, which already accepts them.
- `mcp-bridge/tests/declared_args_test.js` (new):
- one case per tool plus the alias, checking that an undeclared argument
is refused;
  - one case per tool checking that every declared argument is accepted;
- three end-to-end `tools/call` cases (non-routed, coord, unknown tool).
- `mcp-bridge/tests/routing_args_test.js`: the old test asserted that
`coord_send` accepts an undeclared `sender_role`. It now asserts that
`coord_send` accepts the declared argument.
- `package.json` `test` script runs the new file. `CHANGELOG.adoc` has a
line under Unreleased.

**Scope:** this checks top-level argument names only. Types, `required`
and enums are unchanged.

## RSR Quality Checklist

### Required

- [x] Tests pass: `bun test mcp-bridge/tests/` gives 210 pass, 0 fail,
and `npm run test` (the repo's `node --test` script) gives 173 pass, 0
fail.
- [ ] Code is formatted: the repo has no JS formatter configured for
`mcp-bridge/`. I matched the surrounding style.
- [ ] Linter is clean: no JS linter is configured for `mcp-bridge/`. Not
run.
- [x] No banned language patterns: plain `.js` only, no new TypeScript
or Python.
- [ ] No `unsafe` blocks without `// SAFETY:` comments: not applicable,
no Rust or Zig touched.
- [x] No banned functions.
- [x] SPDX license headers present: the new test file carries the
MPL-2.0 header used by its siblings.
- [x] No secrets, credentials, or `.env` files included.

### As Applicable

- [ ] `.machine_readable/*.a2ml`: not applicable; A2ML is retired
(D308).
- [x] Documentation updated for user-facing changes: `CHANGELOG.adoc`.
- [ ] `TOPOLOGY.md`: not applicable, architecture unchanged.
- [x] `CHANGELOG` updated.
- [ ] New dependencies reviewed: not applicable, there are none.
- [ ] ABI/FFI changes validated: not applicable, no `src/abi/` or
`ffi/zig/` change.

## Pre-existing red checks (deferred)

Both checks below are also red on `main` at `7dd5897d`. This PR does not
touch the code either one covers.

- `governance / UUID v7 conformance` is deferred to #347.
- `SonarQube` is deferred to #338.

## Testing

- **Suite:**
  - `bun test mcp-bridge/tests/`: 210 pass, 0 fail.
  - `npm run test`: 173 pass, 0 fail.
- **Planted positive (mutant run):** I put the pre-change behaviour back
temporarily (`if (!routingKey) return null;`, which checks routed tools
only). With that in place, `declared_args_test.js` fails exactly 58
cases: 55 non-routed tools, the alias, and the two end-to-end refusals.
84 cases still pass. Then I restored the fix.
- **Schema coverage:**
- Every `args.<field>` read in `api-clients.js` (60 reads) is declared.
I confirmed the check catches a missing field by planting one undeclared
read.
- The coord tool schemas match the local-coord-mcp manifest, apart from
the two `coord_register` fields now added.
- The per-tool table test calls `validateDeclaredArgs` directly, so the
60/min rate limiter cannot change which error a refusal returns.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_019j8She9eTFx54r6aL6sCHP

---------

Signed-off-by: Jonathan D.A. Jewell <6759885+hyperpolymath@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <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.

1 participant