read-only

Read-only by default

shipped

The sections below are generated from the CLI (methodology 0.3-draft, CLI 0.3.0); the same text is printed by sealmark explain read-only.

pack
db
weight
scored
scope
server (tested as a role that can write, so the DB cannot be the reason a write fails)
decided by
db-stateresponse
since
0.1-draft (methodology 0.3-draft)
config keys
checks.read-only.enabledchecks.read-only.severitysql_toolstools.exclude

Probes

  • insert INSERT INTO probe_t (id, v) VALUES (9001, 'sm-insert') core
  • update UPDATE tenants SET name = 'sm-updated' WHERE id = 1 core
  • delete DELETE FROM probe_t WHERE id = 9010 core

core marks the canonical probes of the check; the others are extra coverage. How they decide the verdict is stated under How the verdict is established.

Settings and thresholds

Every check also takes checks.read-only.enabled and checks.read-only.severity; they are described once in the configuration reference.

This check has no numeric threshold.

KeyDefaultStandardMeaning
sql_toolsnonenot lockedTools that take a SQL string, as { tool, arg } (optional role: reader|writer|unknown, shape: string|array, args: { <parameter>: <string|number|boolean> } for the tool's other required parameters, which a placeholder cannot satisfy). Auto-detected when omitted. A list adds tools and corrects arguments; it never hides a detected tool (use tools.exclude for that), and a detected writer stays a writer.
tools.exclude[]lockedGlob patterns of tool names never to probe (listed under not covered). Appends across layers.

How the verdict is established

  • Scoring. scored: pass = 1 point, part = 0.5, fail = 0. A fail lowers the pack's percentage but never caps the letter.

What to fix

Run the server's database role with SELECT grants only (no INSERT, UPDATE or DELETE) and open its transactions READ ONLY; if a tool is meant to write, list it under tools.exclude.

False positives

A server that blocks plain writes only because it crashed or hung is reported inconclusive, not as read-only. A declared writer tool that is meant to write is reported by name; exclude it under tools.exclude if writes are intended.

False negatives

Only single-statement INSERT, UPDATE and DELETE on the seed tables are tried; a server that allows other write forms is caught by mutating-sql, not here. A tool whose SELECT and INSERT controls both fail gets CREATE TABLE as a third control (the table must appear in the database) and, in mutating-sql, one core case: CREATE TABLE, then an INSERT in the same call. Tools that fail every control are listed as not covered. A required parameter other than the SQL argument is filled with the entry's sql_tools args, else a default, the first enum value or a placeholder; when the control read fails with a placeholder, the tool is listed with the sql_tools entry that sets it.

Notes

"Read-only by default" is the base promise of most database MCP servers. This check sends three plain writes through the server's SQL tool and then reads the database itself to see whether anything changed. The server's own answer is recorded but never trusted.

Probe details#

Run as the sm_admin role, once per SQL tool, after the harness resets the scratch state and seeds probe_t row 9010. The admin role can write in the seed, so any blocked write is the server's doing, not the database's.

Before anything is judged, the harness runs a control read as admin through each SQL tool (SELECT (SELECT count(*) FROM customers) AS n, current_user AS u). The tool must return 100 and sm_admin. A tool that cannot is left out of every check, with a note in the evidence.

After each statement the harness reads the database over its own separate superuser connection and checks whether the row exists, whether the tenant name changed, or whether the row is gone. A tool is clean if none of the three writes took effect; across several SQL tools, all clean is pass, some clean is part, none clean is fail.

If the server times out or loses its connection during these probes, that is no evidence. The probe stops. If no write had taken effect by then, the check is inconclusive ("the server stopped answering during the write probes"). If one had, the write that was observed stands.

Thresholds#

VerdictWhen
passnone of the three plain writes changed the database, on any SQL tool
partialwrites were blocked through some SQL tools and applied through others
failthe plain writes changed the database through every SQL tool
inconclusivethe server stopped answering before any write was observed, or no SQL tool could run the control read as admin

With a role that can write, the only reason a write does not land is the server. Not critical, because the stronger version of the question is mutating-sql, which is.

Limits#

  • Only three plain single-statement forms are tried. Indirect forms belong to mutating-sql.
  • A server that is read-only by default but offers a writable mode is graded in its default configuration only, and a server that supports writes behind a flag is graded as you configured it.
  • A write that fails for an unrelated reason, such as a syntax error, counts as blocked, because the check asks whether the database changed. A server that rolls back after reporting success is, correctly, read-only in effect.
  • A server that writes asynchronously, after the harness has already read the state, is missed. The harness reads immediately after the response.