Add optional pre-execution judgment gate for the tool gateway (opt-in, disabled by default) - #93
Conversation
HttpToolExecutor forwards a tool call's arguments to its execution endpoint after an Operation.Read RBAC check on the ToolResource -- authorization, not content-level judgment on what the specific call actually does. AdapterReverseProxyController's separate reverse-proxy path is stricter still: it forwards the raw request body as an opaque StreamContent and never deserializes it, so no content-level check is possible there even in principle. ReviewGatedToolExecutor is an opt-in IToolExecutor decorator (disabled by default, zero behavior change unless ReviewGate:Enabled=true) that calls an independent judgment endpoint with the tool name + arguments before HttpToolExecutor runs, blocking on a clean high-confidence reject and falling through otherwise (approve, low-confidence/ambiguous reject, or the gate being unavailable -- see the fail-open design note in the new file). 5 offline MSTest cases cover: high-confidence reject blocks and never touches the inner executor; approve falls through; a low-confidence reject falls through (uncertainty, not a clean call); a non-2xx and a timeout from the gate both fail open. No new test-only NuGet dependency -- a minimal fake HttpMessageHandler stands in for Moq.Protected. Full test suite (32/32, including the pre-existing 27) verified green with this change; the sibling Service project builds unaffected. See dotnet/Microsoft.McpGateway.Tools/ReviewGate.md for configuration and a pointer to a live-verification harness run against a real judgment endpoint (not part of this PR / CI -- separate opt-in tooling).
…upstream) ReviewGatedToolExecutor -- an IToolExecutor decorator closing a real, independently verified gap in mcp-gateway's own tool-call path (RBAC-only authorization, no content-level judgment on what a call actually does). Built and tested against a real clone of the target repo (32/32 tests passing), then live-verified against the real production invinoveritas API before a real PR was opened: microsoft/mcp-gateway#93
|
@microsoft-github-policy-service agree |
|
Checked in on this since the workflow run history shows the CI check has never actually executed on the current head ( Verified independently in the meantime, not just asserting it builds: cloned this branch and Happy to adjust scope or split this up if that would make it easier to review. |
What this closes
HttpToolExecutor.ExecuteToolAsync(the built-in "toolgateway" adapter) receives the parsedRequestContext<CallToolRequestParams>— tool name and arguments — checksOperation.ReadRBAC on the registered
ToolResource, then forwards the arguments byte-for-byte to{toolName}-service.adapter.svc.cluster.local. That's an authorization check ("can thisprincipal invoke this tool"), not a content check ("is this specific call, with these specific
arguments, safe to run").
AdapterReverseProxyController's separate proxy path is stricterstill —
HttpProxy.CreateProxiedHttpRequestwraps the request body in a rawStreamContentand never deserializes it, so a content-level check isn't possible there even in principle;
RBAC there is checked once, at the adapter level, before any MCP method is parsed.
Verified directly against current
main(this branch is based on1a66c42, #62) beforewriting anything — not assumed from the README.
Concrete failure shape this leaves open: a principal with valid
Operation.Readaccess toa registered tool (say, a file-delete or shell-exec tool a downstream MCP server legitimately
exposes) gets that call forwarded unmodified regardless of what the arguments actually are.
RBAC answers "may this caller ever call this tool," not "should this particular invocation, with
these particular arguments, go through right now." That's a different, complementary question,
and today nothing in the gateway itself asks it — the two most recent security-hardening PRs
on this repo (#88, #89) tightened the authorization boundary; this is adjacent but orthogonal
to both.
The change
ReviewGatedToolExecutor— anIToolExecutordecorator, registered via one conditional blockin
Program.cs. Disabled by default; identical runtime behavior unlessReviewGate:Enabledis explicitly set to
true. When enabled, it calls an independent judgment endpoint with thetool name + arguments before
HttpToolExecutorruns, blocks on a clean high-confidencereject, and falls through to the real executor on everything else — approve,low-confidence/ambiguous reject, or the gate itself being unavailable (fail-open by design,
documented inline: this repo's execution path is fully automated/Kubernetes-oriented with no
interactive human-in-the-loop surface anywhere, so there's nowhere to hand an uncertain verdict
for a human to resolve — see the file-level comment in
ReviewGatedToolExecutor.cs).It has no opinion on which judgment provider sits behind the HTTP call — the only assumption is
POST {baseUrl}/reviewreturning{"verdict": "approve"|"approve_with_concerns"|"reject", "confidence": number, "summary": string}. Details, config keys, and a pointer to alive-verification harness are in the new
dotnet/Microsoft.McpGateway.Tools/ReviewGate.md.Tested:
approve falls through; low-confidence reject falls through; a non-2xx and a timeout from the
gate both fail open) — no new test-only NuGet dependency, a minimal fake
HttpMessageHandlerstands in forMoq.Protected.Microsoft.McpGateway.Tools.Testssuite: 32/32 green (27 pre-existing + 5 new).Microsoft.McpGateway.Servicebuilds unaffected (this change doesn't touch that project).benign
list_filescall fell through unmodified; arun_shellcall withrm -rf / --no-preserve-rootcame backverdict=reject, confidence=1.00and was blocked.Reference client + the harness: https://github.com/babyblueviper1/invinoveritas/tree/main/integrations/mcp-gateway
Genuine question, not an assertion
Is content-level judgment on tool-call arguments intentionally out of scope for the gateway
itself — i.e. is that expected to live in each downstream MCP/tool server, with the gateway's
job staying strictly routing + RBAC? If so this is happy to be closed / reshaped into a docs
note instead of a code change. If a hook point of this shape is useful, the PR is here to
review as-is — opt-in, off by default, no change to existing deployments.