c8369e37f4
Adds a per-server `trust: full|untrusted` config key (mcp_servers.<name>.trust). On an untrusted server, every write-capable tool call — any tool whose discovery-time annotations do not carry readOnlyHint=True — routes through the existing approval surface (tools.approval.request_elicitation_consent, same lazy-import + surface-routing pattern the MCP elicitation handler uses) before the RPC fires. Denied/cancelled/errored approvals fail closed: the RPC never runs, including the lazy first-use server spawn. Design points: - Classification happens at CALL TIME from metadata captured at DISCOVERY (_record_tool_trust_metadata in _register_server_tools and the lazy cache-registration path). No toolset/schema mutation, so the toolset stays byte-stable and prompt caching is preserved. - readOnlyHint is a server-supplied HINT: on an untrusted server a lying server can at most skip approval for tools it claims read-only — it can never widen access. Trust tiering itself is operator config. - Missing/malformed annotations => write-capable (fail closed). - Unrecognized trust values => untrusted (fail closed); missing key => full (backward compatible, documented in mcp-config-reference). - The schema cache now persists readOnlyHint so lazy-registered servers gate identically on next startup without spawning. Tests: tests/tools/test_mcp_trust_gating.py (11 tests, TDD red->green): approval invoked + accept proceeds, deny/cancel blocks RPC, readOnlyHint =true skips gate, trusted/unconfigured servers skip gate, explicit readOnlyHint=false gated, approval exception fails closed, trust normalization, discovery-time capture (SDK objects and cached dicts). Ported from: cloudflare-os classifyTool() (Apache-2.0), corroborated by Claude Cowork (idea-level).