stacklok / stacklok/mecatl

CallMcpWithQuery weakens target read-only, permission, and dispatch semantics

Open
#1,709 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Go
Stars
152
Forks
16
Avg merge
14h 48m
Merged PRs (30d)
536

Description

Summary

CallMcpWithQuery is intended as a fallback for narrowing an oversized structured MCP result before it enters model context. Its current model-facing and execution semantics instead make it an attractive general-purpose MCP caller, and—more importantly—treat every addressed MCP target as read-only and floor-allowed regardless of the target tool's own annotations or permission behavior.

The wrapper can address any tool exposed by the MCP manager. It unconditionally returns ReadOnly() == true, is registered whenever any MCP tool exists, and has a built-in default Allow. The authority evaluator correctly checks the addressed mcp__<server>__<tool> capability, but permission evaluation and dispatch classification still use the wrapper itself. The remote call occurs before jq filtering, so a remotely mutating target can perform its mutation even though the wrapper was dispatched and permitted as read-only.

Why this matters

A direct mutating MCP tool and the same tool reached through CallMcpWithQuery do not inherit the same safety behavior:

  • A direct MCP tool without an allow rule defaults to an interactive approval.
  • CallMcpWithQuery is floor-allowed.
  • A direct tool without readOnlyHint: true is serialized conservatively.
  • CallMcpWithQuery is always classified as read-only and can be read-parallel dispatched.
  • The wrapper does not limit target selection to MCP tools advertising readOnlyHint: true.

This creates a permission and concurrency bypass for remote side effects. Capability authorization alone does not close the gap when the session legitimately carries the target capability.

Session evidence

Observed in local session 50028801afa1b8a77186921f8e171d66 using mecated v0.0.38-57-g7cf4e5dfd and gpt-5.6-terra:

  1. The model used ten CallMcpWithQuery calls for GitHub pull_request_read rather than the directly exposed GitHub MCP tool.
  2. Every call used jq_filter: ".", so none narrowed its result. The wrapper added no value over the direct call.
  3. The calls ran without permission prompts because CallMcpWithQuery is floor-allowed.
  4. Later direct mcp__github__add_issue_comment calls each produced permission.ask with no permission rule matched mcp__github__add_issue_comment; client approval required.

We did not invoke add_issue_comment through the wrapper, because that would exercise a real remote mutation. The implementation path is sufficient to show that the wrapper accepts the target and calls it before attempting to filter its result.

Contributing usability behavior

The description already says to use the wrapper when a tool returns a large JSON structure and to prefer the remote tool's own pagination/filter parameters. However:

  • The generic name CallMcpWithQuery presents it as a normal MCP calling path.
  • It is disclosed eagerly alongside direct MCP tools rather than after an oversized-result failure.
  • It accepts identity/no-op jq filters such as ..
  • Its floor-allow and read-parallel behavior make it operationally easier than direct calls.

The observed model behavior therefore appears to be encouraged by more than wording alone. Description clarification would help, but would not fix the target-semantics defect.

Expected behavior

Reaching an MCP tool through CallMcpWithQuery must not weaken that target's permission or dispatch semantics. The wrapper should remain a bounded result-projection mechanism, not an alternate authority or mutation path.

Possible design directions:

  1. Restrict CallMcpWithQuery to targets that explicitly advertise readOnlyHint: true; fail closed for missing or false hints.
  2. If mutating targets must be supported, resolve permission and dispatch behavior against the addressed target so the same call asks/denies and serializes exactly as its direct form.
  3. Reject identity/no-op filters and make the error direct the model to the native MCP tool.
  4. Strengthen the description to say: use only after a direct MCP call returns an oversized structured-result error, never for a first attempt.
  5. Consider disclosing the fallback only after the oversized-result path recommends it.

Suggested acceptance coverage

  • A wrapper call to an explicit read-only target remains usable and bounded.
  • A target with readOnlyHint false or absent cannot execute under unconditional read-only/floor-allow semantics.
  • A mutating target, if supported, receives the same permission decision and serial dispatch as its direct namespaced tool.
  • Target-specific configured Ask/Deny rules cannot be bypassed through the wrapper.
  • The remote CallTool operation is never reached when target semantics reject the call.
  • A no-op jq filter such as . is rejected or otherwise cannot serve as a general direct-call substitute.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at the CallMcpWithQuery registration and trace its ReadOnly, authority, permission, dispatch, jq filtering, and CallTool paths. Compare those behaviors with direct namespaced MCP calls, then use the suggested acceptance coverage to verify read-only restrictions, matching permission and dispatch decisions, rejection before CallTool, and no-op filter handling.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
authorization, backend, security
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.