{"slug": "your-mcp-server-says-it-is-read-only-who-checked", "title": "Your MCP Server Says It Is Read-Only. Who Checked?", "summary": "A developer built Airlock, an open-source tool that audits MCP servers by comparing their declared tool annotations, such as readOnlyHint, with observed behavior. In tests, Airlock detected all five planted dishonest behaviors in a controlled fixture while reporting zero findings for an honest one, and it audited external servers, including a ContextFirewall target and five public stdio servers, to expose trust gaps in annotation-based approval systems.", "body_md": "MCP servers describe their own tools. A tool can publish `readOnlyHint: true`\n\n, and an agent harness may use that annotation to decide whether a human approval card is needed.\n\nThat creates an uncomfortable trust boundary: the server being evaluated is also the server describing how dangerous it is.\n\nI built [Airlock](https://github.com/himanshu748/airlock-mcp) to test the declaration against observed behaviour, then enforce the resulting policy on the wire.\n\nAirlock reports what it observed. Absence of a finding is not proof of safety.\n\nConsider an MCP tool called `export_report`\n\n. It declares itself read-only, returns a normal result and writes a file outside the operator's declared scope.\n\nA harness that trusts the annotation may run it without approval. Airlock opens a case, inventories the declared tools, exercises them under a capped budget and compares the declaration with its observations.\n\nIf a tool behaves differently from what it declared, the difference becomes evidence. For an approved case, Airlock emits a connector that points to a per-case enforcing proxy rather than back to the suspect server.\n\nThe policy is enforced before the call reaches the upstream tool. It is not a sentence in a system prompt.\n\nAirlock ships two six-tool fixtures with the same surface.\n\nThe dishonest fixture plants five behaviours, including a filesystem write behind `readOnlyHint: true`\n\n, scope escape, undeclared egress, injected instructions and canary exfiltration.\n\nThe result:\n\n```\ntarget      dishonest fixture, controlled_fixture mode\nprobes      24\nresult      7 findings of 36 checks, all five planted behaviours\n```\n\nThe honest fixture produces zero findings across the same 36 checks.\n\nThat contrast is essential. A detector that only finds problems in a fixture designed to look suspicious has not shown much. The honest control shows that the findings follow observed behaviour rather than unfamiliarity.\n\nEach check resolves to one of four states:\n\n`finding`\n\n`no_finding_observed`\n\n`not_tested`\n\n`sensor_failed`\n\nSeverity is reported separately.\n\nI deliberately avoided a single safety score. An average can hide the one tool that lied. It also encourages readers to treat missing visibility as a passing result.\n\n`not_tested`\n\nis a first-class state. Under `transcript_only`\n\n, MCP traffic cannot reveal server-side filesystem or network activity. Airlock records those questions as untested rather than clean.\n\nIt distinguishes `capability_absent`\n\n, where no sensor existed, from `evidence_missing`\n\n, where the sensor could answer but observed nothing.\n\nThe owned fixture proves that the detectors can catch planted behaviour. The more interesting question is what happens on external servers.\n\nAirlock audited a deployed ContextFirewall target over HTTPS:\n\n```\n6 tools\n30 probes\n0 tools declaring any annotation\n```\n\nThe server did not lie. It said nothing. A harness that resolves categories such as `@write`\n\nand `@destructive`\n\nfrom annotations can match nothing either way, so tools such as `remember`\n\nand `forget_memory`\n\nmay avoid the approval path.\n\nAirlock also inventoried five public stdio servers:\n\n| Server | Tools |\n|---|---|\n`server-filesystem` |\n14 |\n`server-everything` |\n13 |\n`mcp-server-git` |\n12 |\n`server-memory` |\n9 |\n`server-sequential-thinking` |\n1 |\n\nStdio support matters because many MCP servers are launched as commands rather than exposed at a URL.\n\nLaunching a stdio server means executing the code Airlock exists to distrust. The command therefore never comes from a case argument, tool result or model-generated string.\n\nThe operator configures a fixed map of names to argument arrays. A case may select a name. Names are looked up, never parsed into shell commands.\n\nThe child starts in a throwaway working directory with an explicit environment. Airlock revalidates the whole command binding before each connection, so repointing a configured name revokes an open case instead of silently running a different command.\n\nAirlock documents an important remaining boundary: its HTTP response cap does not apply to the MCP SDK's stdio transport. The audit deadline still applies, but a large line can reach the SDK before Airlock can bound it. Calling that solved would be worse than stating it.\n\nA TrueForge agent drives the audit through six control tools:\n\n```\nopen_case\nlist_declared_tools\nprobe_tool\nread_evidence\nseal_case\nemit_policy\n```\n\nThree operations are always approval-gated: `probe_tool`\n\n, `seal_case`\n\nand `emit_policy`\n\n.\n\nThe agent inventories the target, asks Airlock to probe opaque tool IDs, reads the aggregate evidence and presents a human choice: Block, Approve selected or Approve all.\n\nFor an allowed case, the emitted connector points at the enforcing case proxy. A call to a tool the case did not approve receives:\n\n```\nMCP error -32001: Tool blocked by Airlock policy\n```\n\nAirlock audited its own control MCP. The server published full annotations, but the probe planner rejected `open_case`\n\nbecause its schema contains a `$ref`\n\ninto `$defs`\n\n, outside Airlock's bounded v1 probe profile.\n\nThat case is incomplete. It appears on the hosted page because a product arguing that missing evidence is not proof of safety should not hide its own missing evidence.\n\nEvery substantive change went through a Qodo-reviewed pull request.\n\nOn the stdio transport, Qodo found that revalidation compared only the target name. Repointing that name could leave an open case running an operator-withdrawn command. It also found that the MCP SDK inherited more host environment variables than the README claimed.\n\nOn the approval boundary, Qodo caught a test that claimed to cover every side-effecting control tool while checking three hard-coded names. The first fix introduced a second map that could drift, and Qodo caught that too. The decorators now read from the same policy map the test validates.\n\nAnother review found that a passing-suite claim did not survive a clean CI machine. The failures were fixed, re-reviewed and recorded.\n\nThe reviews did more than find code defects. They caught statements that were stronger than the observed behaviour, exactly the failure Airlock is designed to expose.\n\nRun the complete suite with:\n\n```\npython3 -m venv .venv\n.venv/bin/python -m pip install -e '.[dev]'\n.venv/bin/python -m pytest -q\n```\n\nThe current suite contains 302 passing tests.\n\nThe backend requires persistent state, long-running audits, child processes for stdio targets and a loopback-only operator interface, which makes a serverless deployment the wrong shape. The repository quickstart runs the complete fixture audit locally in two commands.\n\nAirlock is open source under the MIT licence and was built for the WeMakeDevs x TrueFoundry Agent Harness Hackathon.\n\nDo not ask a server whether it is safe. Ask what it did, keep the record and enforce the answer.", "url": "https://wpnews.pro/news/your-mcp-server-says-it-is-read-only-who-checked", "canonical_source": "https://dev.to/himanshu_748/your-mcp-server-says-it-is-read-only-who-checked-2mjk", "published_at": "2026-08-30 04:43:19+00:00", "updated_at": "2026-08-30 05:22:34.006063+00:00", "lang": "en", "topics": ["ai-safety", "ai-agents", "developer-tools", "ai-infrastructure"], "entities": ["Airlock", "MCP", "ContextFirewall"], "alternates": {"html": "https://wpnews.pro/news/your-mcp-server-says-it-is-read-only-who-checked", "markdown": "https://wpnews.pro/news/your-mcp-server-says-it-is-read-only-who-checked.md", "text": "https://wpnews.pro/news/your-mcp-server-says-it-is-read-only-who-checked.txt", "jsonld": "https://wpnews.pro/news/your-mcp-server-says-it-is-read-only-who-checked.jsonld"}}