You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 68c9e96
Browse filesBrowse the repository at this point in the historyBrowse files
fix(mcp-bridge): refuse undeclared arguments on every tool (#359)
## 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>
variant: {type: "string",description: "Optional free-form model/variant label set at register time (Task #33). Alphanumeric + `.`/`-`/`_`, max 32 bytes. e.g. `opus-4.7`, `flash-2.5`, `leanstral`. Equivalent to a follow-up `coord_set_variant` call.",maxLength: 32,pattern: "^[A-Za-z0-9._-]*$"},
596
+
role: {type: "string",enum: ["journeyman","apprentice","executor","supervised"],description: "Optional requested role (DD-32). `master` is always refused here; promote via `coord_promote_to_master`. Default: claude → journeyman, others → apprentice. `executor`/`supervised` are legacy aliases."},
597
+
capabilities: {
598
+
type: "object",
599
+
description: "Optional capability advertisement for cold-start routing. Each key is optional; equivalent to a follow-up `coord_set_capabilities` call.",
600
+
properties: {
601
+
class: {type: "array",items: {type: "string"},description: "Capability classes (e.g. 'reasoning', 'coding', 'proof'). Max 128 bytes as CSV."},
prover_strengths: {type: "array",items: {type: "string"},description: "Prover names this peer claims strength in (e.g. 'coq', 'lean'). Max 256 bytes as CSV."},
604
+
},
605
+
},
596
606
},
597
607
required: ["client_kind"],
598
608
additionalProperties: false,
@@ -642,6 +652,7 @@ function buildToolList(scope) {
642
652
token: {type: "string",description: "Session token from `coord_register`."},
643
653
target: {type: "string",description: "Peer ID to send to (e.g. `claude-a1b2@repo`), or `*` for broadcast to all active peers."},
644
654
message: {type: "string",description: "Message payload — free-form text, typically a JSON A2ML envelope.",maxLength: 65536},
655
+
sender_role: {type: "string",enum: ["master","journeyman","apprentice","executor","supervised"],description: "Optional sender role for envelope contract validation, used when the envelope's own `_meta.sender_role` is absent."},
645
656
},
646
657
required: ["token","target","message"],
647
658
additionalProperties: false,
@@ -790,6 +801,7 @@ function buildToolList(scope) {
790
801
target: {type: "string",description: "Peer ID to send to, or `*` for broadcast."},
791
802
message: {type: "string",description: "Message payload — typically a JSON A2ML envelope. Validated against `coord-messages.ncl` when strict mode is enabled.",maxLength: 65536},
sender_role: {type: "string",enum: ["master","journeyman","apprentice","executor","supervised"],description: "Optional sender role for envelope contract validation, used when the envelope's own `_meta.sender_role` is absent."},
0 commit comments