Research / Agents and LLMs

P57 · tool-demo · draft

Derived MCP Hints and a Gating Proxy: Annotations Derived From Checked Emission Classification

partial tool-demo Agents and LLMs

State of the work. Hint derivation and the gating proxy ship on main with 26 proxy tests and 8 adversarial hint tests; no corpus evaluation, no overhead measurement and no mechanized proof of the hint projection yet.

This is a draft, not peer reviewed. Repository references link to revl at commit 45bec431f.

Pitch

An MCP tool's readOnlyHint can be a compiler-derived fact instead of an author's assertion, and the same classifier can run as a proxy that refuses to believe the hints of servers it did not compile.

Abstract

The Model Context Protocol lets a server annotate each tool with readOnlyHint and destructiveHint. These are assertions by the server author. Nothing checks them, and tool-poisoning attacks exploit that gap. We describe a design in which the hints are derived rather than declared. In revl, a service operation carries a checked effect class (plain, witnessed, emission); the checker refuses any provider that reaches a boundary the declaration did not admit, and the emission analysis is propagated transitively, including through first-class function values. The service-to-tool projection reads the class and emits readOnlyHint and destructiveHint from it, together with provenance (x-revl). For servers we did not compile, revl mcp proxy sits between an unmodified MCP server and its client, classifies the server's tool list with the same classifier used by revl mcp import, and gates every tools/call. The proxy treats an uncontradicted readOnlyHint: true as unchecked and holds it behind an approval ticket by default; trust is opt-in per process (--trust-read-only-hints). Declared contradictions are caught at startup and a resource-change notification observed during a "read-only" call withdraws the claim at run time. We list what the proxy guarantees and, as the docs do, what it cannot: the truth of an opaque upstream's claim. The implementation is in the revl tree with 26 proxy tests and 8 adversarial hint tests. We report no user study or attack benchmark yet.

Problem and claim

MCP clients use readOnlyHint and destructiveHint to decide what to show a human and what to auto-approve. The annotations are written by the same party whose tool is being trusted. A poisoned or buggy server can mark a mutating tool read-only, and the client has no way to tell.

The claim has two halves. First, for tools that revl itself compiles, the hints are consequences of the checker: a tool is read-only because the compiler refused to compile its provider otherwise (src/revl/mcp/composed.py:9-16, src/revl/mcp/schema.py:159-168). Second, for tools revl did not compile, a gateway can be sound in one specific direction: no tool is more permissive under the proxy than the importer would make it, and the one thing it cannot check (the upstream's read-only claim) is gated by default rather than trusted (docs/mcp-proxy.md:195-222).

The formal development proves the checker's G4 classification (formal/STATUS.md:260-266), but no mechanized theorem covers the projection from that class to readOnlyHint. The "derived" claim is design-level until one does.

Contributions

  1. A projection from a revl service declaration to an MCP tool definition in which readOnlyHint and destructiveHint are computed from the checked effect class, openWorldHint from extern reachability and idempotentHint from a commutative declaration, with an x-revl provenance block (src/revl/mcp/schema.py:40, :156-190).
  2. A soundness repair story for that projection: an emission hidden behind a first-class function value used to compile with readOnlyHint: true and fire at run time; the fix treats a bare reference to an emitting callable as reaching an unnameable boundary, with adversarial tests (tests/test_mcp_hint_adversarial.py:1-20, docs/mcp-bridge.md:99-120).
  3. A single classifier, classify_imported_tools, shared by revl mcp import and revl mcp proxy, with an explicit trust direction (MCP to revl: nothing vouched for, only an uncontradicted read-only claim avoids emission) (src/revl/mcp/schema.py:530-560).
  4. A gating proxy for an unmodified stdio MCP server: no .rvl written; every tools/call routed through Session.call; unchecked read-only claims gated by default; declared and observed contradiction; operator-declared --undo making a tool witnessed (docs/mcp-proxy.md:58-153).
  5. An explicit guarantee and non-guarantee list for the proxy, including the advisory status of the gate without an operator profile (docs/mcp-proxy.md:86-92, :195-256).

Evidence on main

Claimpath:line or testStatusNote
Hints are derived from the checked class, not assertedsrc/revl/mcp/schema.py:156-168; src/revl/mcp/composed.py:9-16shippedreadOnlyHint is false for witnessed and for local writes; destructiveHint true for emission or local writes
Provenance block with classification and capabilitiessrc/revl/mcp/schema.py:170-185; docs/mcp-bridge.md:122-133shipped
First-class emission smuggling was a real soundness bug and is fixedtests/test_mcp_hint_adversarial.py:1-20, 54-178; docs/mcp-bridge.md:99-120shipped8 tests; the doc admits the cost: a dispatcher that ever receives an emitting function is may-emit for every caller
Service declaration is an upper bound on providersdocs/mcp-bridge.md:82-95shippedG4 diagnostic quoted in the doc
Verb-level annotations on the compiler's own MCP server are "derived from what the handler does"docs/mcp-reference.md:57-60, table at :215-232shipped (documented); derivation method not checkedThe doc says derived, not how
One classifier for import and proxysrc/revl/mcp/schema.py:530; src/revl/mcp/proxy.py:369; tests/test_mcp_proxy.py:79shipped
Unchecked read-only claim gated by default; opt-in flagdocs/mcp-proxy.md:125-146; src/revl/cli/interop.py:478; tests/test_mcp_proxy.py:68, 257, 277, 480shipped
Declared contradictionsrc/revl/mcp/schema.py:515-525; tests/test_mcp_proxy.py:118-135shippeddestructiveHint: true, revl provenance saying emission, or an operator-declared undo
Observed contradiction (resource-change notification during a read-only call)docs/mcp-proxy.md:107-116; tests/test_mcp_proxy.py:155, 312, 338, 416shipped
Unclassifiable tool takes the most restrictive classdocs/mcp-proxy.md:75-84; tests/test_mcp_proxy.py:136-153, 407shipped
Witnessed undo on abort, commit keepsdocs/mcp-proxy.md:155-178; tests/test_mcp_proxy.py:353, 377shipped
E-Stop refuses later calls before the upstreamtests/test_mcp_proxy.py:388shipped
CLI stdio round trip and abort on disconnecttests/test_mcp_proxy.py:437shipped
Upstream text is never compiled as codedocs/mcp-proxy.md:217-221shipped (documented)The renderer was not read to confirm
Mechanized theorems for the checker's G4 classificationformal/RevL/Theorems/G4_ClassifiedInverseOrEmit.lean; formal/STATUS.md:260-266 (RevL.G4Classified.inverse_or_emit_classified, reached_crossing_carries_inverse_or_marker, fn_wrapper_still_crosses)partialProves the inverse-or-emit classification of the checker. It does not cover the projection to readOnlyHint/destructiveHint
Mechanized theorem for the hint projection (readOnlyHint implies no emission reach)none found under formal/designedThe projection rests on construction in tools_from_ir, not on a proof
Proxy gate is advisory without an operator profiledocs/mcp-proxy.md:86-92, 238-239shippedStated by the docs

Numbers that exist

Paper outline

  1. Introduction: hints as assertions, the poisoning gap.
  2. Background: MCP annotations; revl effect classes (plain, witnessed, emission) and G4.
  3. Deriving hints: projection, provenance, the first-class-value hole and its fix.
  4. The proxy: classifier, trust direction, read-only honesty (declared, observed, unchecked), default gating.
  5. Undo, commit, abort and E-Stop through a foreign server.
  6. Guarantees and non-guarantees.
  7. Evaluation (to be run).
  8. Related work and discussion.

Threats to validity and limits

Fact-check

Claims in this draft were checked against revl main at commit 45bec431f: 23 checked, 9 corrected.

Cite this draft

@misc{revlp57,
  author       = {Thomas Moussajee},
  title        = {Derived MCP Hints and a Gating Proxy: Annotations Derived From Checked Emission Classification},
  year         = {2026},
  howpublished = {\url{https://inso1337.github.io/revl/research/P57.html}},
  note         = {Research draft, not peer reviewed. Written against revl commit 45bec431f. Licensed CC BY 4.0.}
}

Text licensed CC BY 4.0, author Thomas Moussajee.

← All research drafts