dbprint MCP server — v1
User-facing specification for dbprint's Model Context Protocol (MCP) surface. The MCP server exposes committed prints as native AI-tool primitives — Resources for the per-table artifacts, Tools for computed and search operations. Any MCP-aware client (editor, agent runtime, custom integration) can consume dbprint output through this protocol.
Any tool that implements this surface MUST comply with the requirements below. Conformance MUST be mechanically verifiable against the resource enumeration, tool signatures, and error model.
0. Scope and terminology
0.1 What this spec covers
- Server identity advertised at MCP handshake.
- The Resource URI scheme and per-artifact resource enumeration.
- Tool signatures, input/output schemas, and return shapes.
- Multi-connection model and the default-connection rules for tool calls.
- Transport options (stdio default, HTTP loopback).
- Lifecycle behaviors (file freshness, shutdown, capability flags).
- Error model mapped to JSON-RPC standard codes.
0.2 What this spec does NOT cover
- The implementation of
dbprint serve(the CLI wrapper and SDK integration). - The format of
manifest.yaml/diff.yaml/ per-table artifacts — covered byformat/v1/SPEC.md. - Authentication, authorization, or remote deployment models.
- Write operations via MCP.
- The MCP Prompts primitive.
- Resource subscriptions or live-change notifications.
0.3 Terminology
- MCP: Model Context Protocol — the open protocol clients use to discover and invoke tools and resources from servers.
- Resource: an MCP primitive representing static-data-by-URI; enumerable via
resources/list, readable viaresources/read. - Tool: an MCP primitive representing a parameterized operation; invocable via
tools/call. - Transport: the wire protocol carrying MCP frames (stdio or HTTP / SSE).
- Capabilities: the feature flags a server advertises at handshake.
- Connection: a dbprint connection name as configured in
.dbprint.yaml. A single server may serve one or many connections. - Default connection: the connection a tool call falls back to when its
connection?parameter is omitted.
0.4 Requirement levels
This document uses MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY per RFC 2119.
1. Distribution
The MCP server is gated on the [mcp] install extra:
pip install "dbprint[mcp]"The extra adds a dependency on the official Anthropic Python mcp SDK. Without the extra, dbprint serve MUST exit with code 1 and a clear install hint at command invocation time; no MCP handshake is attempted.
The server is read-only against committed prints — no database connection is opened. No adapter extra is required for dbprint serve.
2. Server identity and handshake
At MCP handshake the server MUST advertise:
{ "serverInfo": { "name": "dbprint", "version": "<package version>" }, "instructions": "Answers questions about a database from its print ... (abridged; described below)", "capabilities": { "resources": { "subscribe": false, "listChanged": false }, "tools": { "listChanged": false } }}MCP's InitializeResult carries no description field — serverInfo is name/version only, and free-text server description travels as the top-level instructions string instead. instructions is delivered unprompted on every connect and opens with the routing: which tool answers which task — writing SQL, resolving a filter value's spelling, locating a column, listing tables and their freshness, what changed since the previous run, what a field or spec_ref means, how to read a json/yaml field, the raw manifest — naming every tool, and that the tools, not the print's files, are the route, since a file read directly carries none of the interpretation the tools apply. The rules for reading an answer follow: which population a number describes (scope), that inferred fields and inferred or measured relationships are guesses, that an absent field is not zero, and that the print states what the data holds, not what its codes, units or defaults mean. The whole text stays under 2,048 characters, the length at which Claude Code truncates a server's instructions by default. Some clients ignore instructions entirely, so every tool description below stands alone rather than depending on it having been read: each states the task it answers, when to prefer it, and which sibling answers a neighbouring task.
version MUST equal the installed dbprint package version (importlib.metadata.version("dbprint")).
Capability values are normative:
| Capability | Value | Meaning |
|---|---|---|
resources.subscribe | false | No per-URI subscription support |
resources.listChanged | false | No notifications when the resource set changes |
tools.listChanged | false | No notifications when the tool set changes |
The server MUST NOT advertise prompts or sampling capabilities.
3. Resources
3.1 URI scheme
Every resource URI is of the form:
dbprint://<connection>/<rest>where <connection> is a connection name from .dbprint.yaml and <rest> identifies the artifact. The URI scheme dbprint:// is reserved for this server.
A second form carries an empty authority:
dbprint:///reference/<document>This addresses a server-global resource that belongs to no connection - §3.2's two reference documents.
Either form takes an optional query naming one page of the resource (§3.4): ?page=<n>, and &version=<v> on any page past the first. The bare URI is page 1. page and version are the only query keys; any other is a malformed URI. An empty authority MUST NOT be read as a zero-length connection name: a connection name is never empty in practice, and this form is checked ahead of any connection-scoped one, so it can never collide with one — not even with a connection named reference.
3.2 Per-artifact resource list
The server MUST expose the following resources for every served connection:
| URI pattern | mimeType | Source file |
|---|---|---|
dbprint://<connection>/manifest | application/yaml | prints/<connection>/manifest.yaml |
dbprint://<connection>/diff | application/yaml | prints/<connection>/diff.yaml, without its events about an edge a human rejected (§3.4) |
dbprint://<connection>/reading | text/markdown | prints/<connection>/reading.md |
dbprint://<connection>/manifest_annotations | application/yaml | prints/<connection>/manifest.annotations.yaml (present only when authored) |
dbprint://<connection>/<table>/ddl | application/sql | per-table ddl.sql |
dbprint://<connection>/<table>/statistics | application/yaml | per-table statistics.yaml |
dbprint://<connection>/<table>/relationships | application/yaml | per-table relationships.yaml, without the edges a human rejected (§3.4) |
dbprint://<connection>/<table>/description | text/markdown | per-table description.md (present only when authored) |
dbprint://<connection>/<table>/statistics_annotations | application/yaml | per-table statistics.annotations.yaml (present only when authored) |
dbprint://<connection>/<table>/relationships_annotations | application/yaml | per-table relationships.annotations.yaml (present only when authored) |
<table> is the dotted fully-qualified name (arboretum.seedbank.accession), NOT the slash-delimited filesystem path. Agents already know FQNs from prompts; the URI keeps that mental model intact. A slash-delimited form (dbprint://<connection>/arboretum/seedbank/accession/statistics) names the same table: no segment contains . (SPEC 1.5.1), so the two spellings cannot diverge.
The server MUST also expose these two server-global resources, one per document, regardless of which or how many connections are served:
| URI pattern | mimeType | Source |
|---|---|---|
dbprint:///reference/spec | text/markdown | the packaged format specification, whole |
dbprint:///reference/assertions | text/markdown | the packaged assertion DSL specification, whole |
§4.6's get_reference tool addresses a section of either document directly; these two resources exist for browsing the whole thing.
3.3 Enumeration
resources/list MUST enumerate every resource for every served connection:
manifest,diffandreadingresources for each connection (exactly 3 per connection).manifest_annotationsis listed ONLY whenmanifest.annotations.yamlis present at the connection root (§2.7.3 of the format spec).- Per-table resources for every table in each connection's
manifest.yamltablesmap that a reader can follow; an entry that is not a mapping, whosepathis not a string, or whosepath, directory or a declared file leads outside the connection root (format spec §2.5), lists nothing. A connection whose manifest cannot be read lists no per-table resources and keeps itsmanifestresource, whose read names the error; every other connection lists as usual. description,statistics_annotationsandrelationships_annotationsresources are listed ONLY when the corresponding per-table file (description.md/statistics.annotations.yaml/relationships.annotations.yaml) is present.- The two reference resources are listed exactly once each — server-global, never once per connection, and present even when the resolved connection set is empty.
resources/list honours the protocol's own cursor: a reply whose serialized entries would pass 20,000 characters carries the entries that fit and nextCursor, and passing that back returns the next run of the same order. Every page but the last carries nextCursor. The cursor is pinned to every served connection's manifest.yaml as §4.8 pins a tool's cursor; one followed after any of them changed is -32602 InvalidParams saying the print changed. tools/list pages by the same rule, and its tools fit one page.
Resource entries returned by resources/list include uri, name (human-readable label), description (what the resource holds and, for a per-table file, which tool returns the same content interpreted), and mimeType. Ordering MUST be deterministic: the two reference resources first (spec then assertions), then by connection name, then by FQN, then by artifact name (ddl -> statistics -> relationships -> description -> statistics_annotations -> relationships_annotations).
3.4 Reading
resources/read returns the file content with the matching mimeType. Behaviors:
- The server MUST serve the file as it is on disk at the time of the call; see §7.1 for the freshness contract.
- The
relationshipsanddiffresources are the exceptions: an edge a human rejected inrelationships.annotations.yaml— the referencing table's own file decides it, from either side — is withheld from bothrefers_toandreferenced_by, anddiffdrops every relationship event about it, withsummary.relationships_changedcounting the events kept, exactly asget_diff(§4.5) does; the rest is served as YAML. The pages concatenate to that served text, andversioncovers every annotation file that decided it, so a verdict edited between two pages invalidates the later read. A file no rejection touches is served byte for byte. - A file whose text, serialized as a JSON string, passes 20,000 characters is read a page at a time. Pages break at line boundaries, and a line longer than a page at a character boundary; the pages' text concatenated in order is the file byte for byte. Every read carries
_meta: {"page": <n>, "pages": <m>, "version": "<v>"}beside its text, whereversionidentifies the file as it was read. Pagenpast the first is read as<uri>?page=<n>&version=<v>, with theversionpage 1 reported. - A
pagethat is not a whole number from 1 to the resource's page count MUST return-32602 InvalidParamsnaming the valid range. A page past the first asked for withoutversionMUST return-32602 InvalidParams, and so MUST aversionthe file has moved past since it was read — a rewritten file never answers an old page number with new text. - Reading a missing
description.mdorstatistics.annotations.yamlMUST return JSON-RPC error-32602 InvalidParamswith detail explaining the resource is optional and not authored for this table. - Reading a manifest-listed artifact whose file is missing from disk MUST return JSON-RPC error
-32603 InternalErrorwith detail recommendingdbprint generate. - YAML parse failures MUST surface as
-32603 InternalErrorwith the parser's error message and the affected file path.
4. Tools
Every tool in tools/list carries annotations with all four behaviour hints as explicit booleans: readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false. No tool writes anything, and every tool reads only the served prints and the packaged reference documents.
4.1 get_table_context
Returns an assembled context fragment for one table, formatted for direct insertion into an LLM prompt — the tool to read before writing SQL against a table (purpose: query) or answering a question about its data (purpose: profile). Nothing is dropped to fit a client: a context larger than a page is paged (§4.8), section by section. Under profile the Markdown Notes summarise a value list that is not the column's whole domain as its 5 most frequent values, each with its share of the non-null scanned rows and the share the five cover together; json and yaml carry the list whole.
purpose selects what the fragment is for, and is the first thing a caller decides:
purpose | Sections | For |
|---|---|---|
profile (default) | Header, Terms, Connection notes, DDL, Description, Annotations, Cardinality table, Relationships | Describing the data: what was measured and how much of it |
query | Header (identity + scope), Terms, DDL, Connection notes, Joins, Data dictionary, Column values | Writing SQL against the table: what the columns mean, what they join to, and which literals they hold |
The Markdown reply opens, right after its header, with ## Terms: one - <label>: <definition> line for every label, flag and enum word the reply prints and no other, each naming where json/yaml carries it — long tail names distribution: long_tail, P50 names percentiles.p50 — and the search_columns filter that takes it, where one does. format: "json" and "yaml" carry no legend.
Under both purposes, ## Connection notes carries the connection's manifest.annotations.yaml notes (format spec §2.7.3) — a fact many tables share, such as what a code means — whenever the file holds any and include_annotations is true; the section is absent otherwise.
Under both purposes, a table with no description.md and no column note or value note in statistics.annotations.yaml carries the header line Meanings: not documented here - read codes, units and defaults from the code: the print states which values the data holds, not what they mean. format: "json" and "yaml" carry the same fact as meanings_documented: false among the identity fields, absent when the table is documented. Under query, a categorical or text column whose value list is complete, that is not redacted, and whose entry carries neither a note nor a value note appends ; meanings: not documented here to its coverage cell, and its values block carries meanings_documented: false.
Under query, a column whose statistics.annotations.yaml entry carries derived_from (format spec §2.7.1), or whose statistics.yaml column carries a parsed derived_from (format spec §2.2.4), carries its sources' notes in ## Data dictionary: one - <column> (from <table>.<column>): <note>; <value> = <note> line per source the print holds whose own entry carries a note or value notes, after the column's own line. An annotation's own derived_from replaces the parsed one for its column. A source's own derived_from is not followed, and a source absent from the print carries nothing. format: "json" and "yaml" carry the same as inherited_notes: per column, a list of {source, note, values}, each key present only when the source carries it. Under query a column with inherited notes reads as documented for the Meanings cues above; under profile, which does not render them, it does not. include_annotations: false drops them with every other annotation.
Under query the fragment carries no statistics beyond the null shares, no null patterns and no physical layout. The join paths are the ## Joins list: every edge in relationships.yaml except one a human rejected, plus every edge a human added in relationships.annotations.yaml (format spec §2.7.2) — listed on both tables it joins with the detection annotated, never declared — one line each as <column> -> <table>.<column> (<detection>) or <column> <- <table>.<column> (<detection>), so an edge the print inferred or measured on a table whose catalog declares no key is still listed — and nothing measured about an edge (fan-out, coverage, referential actions) is included. A path-valued endpoint (format spec §2.3.9) is written <column> (path <key>.<key>). A -> line whose referencing column is a single column appends that column's null share as ; nulls: <share>; a path-valued referencing endpoint appends instead the share of scanned rows whose key reads NULL (the column null, the key absent, or the key holding a JSON null), derived from the key's part (format spec §2.2.18), and appends nothing when the print lists no part for that key — the column's own null share is then stated as for any column no Joins line names alone. An edge carrying a human's note appends ; note: <note> on one line — a note on an edge the print emitted rides on that edge's own line rather than adding a second one. An edge a human rejected in relationships.annotations.yaml is not listed, and a join description.md describes only in prose is not an edge. A table with no edge renders ## Joins and none found, followed by (not a join target, no declared-unique column) when eligible_target is false; the section is absent only when relationships.yaml was not read or include_relationships is false.
The value table is what an exact-match predicate is written from, and it carries only the lists a predicate can be written from: a column whose values_coverage is 1.0 is rendered in full, one <value> (<count>) entry per value, with the coverage cell stating that the list is the column's whole domain; a column with a coverage below 1.0 shows its five most frequent values (a spelling group counting as one), with the coverage cell stating, as a percentage, the share of the column those five cover and that they are a sample. A numeric or temporal column carries no values_coverage, since its list is a frequency sample and never a domain (SPEC 2.2.3); it has a row only for the listed values that each hold at least 1% of its non-null scanned rows and at least twice the share an even spread over its cardinality would give one value — a sentinel such as '2099-12-31' or -1 — at most five, with the coverage cell stating their joint share and frequent values, not a domain. Such a column with no value that clears both bars has no row. A value carrying a note in statistics.annotations.yaml renders it inline (<value> (<count>) = <note>); a redacted column publishes its counts and no literal; a scoped table's exhaustive list says so over the rows scanned, never over the table. Every DDL-nullable column states its null share exactly once — 0.4%, or none (none over the rows scanned on a scoped table) when the rows read hold no null: in the table's Nulls cell when it has a row, on its Joins line when one names it alone, and otherwise on one Nulls: <column> (<share>), ... line closing the ## Column values section, which holds that line alone when no column has a row. A NOT NULL column states nothing — an empty Nulls cell where it has a row. A value is spelled so it reads back as itself: every string and timestamp is an SQL literal, single-quoted with an embedded ' doubled; a number, a boolean and NULL are bare; and a string holding a line break, control or invisible character is double-quoted with YAML escapes, always on one line — NULL is a genuine null, 'NULL' the stored string. List entries are separated by , and the facts of a Notes cell or a line by ; . Every number in the markdown is spelled as statistics.yaml spells it — positional, never in exponent form, counts as plain digits — and a share strictly between 0 and 1 never rounds onto 0% or 100%.
On a table whose statistics.yaml carries scope (SPEC 2.2.8), the format: "json"/"yaml" object carries that block verbatim as scope, beside row_count, at its top level under both purposes, so include_stats: false, which drops the statistics object, never drops the population it describes. Under profile Markdown, every claim a single unread row could falsify — a complete value list, a candidate key, a freshness verdict, nulls: none — carries over the rows scanned in its own Notes cell.
include_ddl, include_description, include_annotations and include_relationships narrow the query selection the way they narrow profile — include_relationships governs the Joins list; include_stats has nothing to drop there.
{ "name": "get_table_context", "inputSchema": { "type": "object", "additionalProperties": false, "properties": { "table": { "type": "string", "minLength": 1, "description": "Dotted fully-qualified table name, as list_tables and search_columns return it" }, "connection": { "type": "string", "description": "Which database to read, by connection name; needed only when the server serves several, and the error for a missing one lists them. Omit it for the server's default" }, "purpose": { "type": "string", "enum": ["profile", "query"], "default": "profile", "description": "query before writing SQL: DDL, the Joins list, data dictionary and the value lists with counts and coverage, and each nullable column's null share, with no other statistics. profile (default) to describe the data: statistics, relationships, notes" }, "format": { "type": "string", "enum": ["md", "json", "yaml"], "default": "md", "description": "md renders the chosen purpose as Markdown - under `profile`, a per-column Notes summary rather than the raw statistics fields json and yaml carry. All three omit each column's sketch (a hashed summary of its distinct values); the table's raw statistics file, sketch included, is the dbprint://<connection>/<table>/statistics resource. get_reference document: guide explains each json/yaml field." }, "include_stats": { "type": "boolean", "default": true, "description": "Include the Cardinality table (md) or statistics object (json/yaml); no effect under `query`, which carries neither" }, "include_relationships": { "type": "boolean", "default": true, "description": "Include the Relationships section (md) or relationships object (json/yaml); under `query`, the Joins list" }, "include_description": { "type": "boolean", "default": true, "description": "Include the table's prose description, when one was written" }, "include_annotations": { "type": "boolean", "default": true, "description": "Include the notes and claims people wrote on its columns and values, and notes that apply to the whole database, when written" }, "cursor": { "type": "string", "minLength": 1, "description": "`next_cursor` from this call's previous page; omit for the first page" } }, "required": ["table"] }}Return:
format: "md"-> a markdown string carrying the chosen purpose's sections.format: "json"-> a structured object. Both purposes carryconnection_notes, the notes text, under the same condition as the Markdown section. Underprofile:table,ddl,description,annotations,statistics,relationships—relationships.yamlless the edges a human rejected, plus each edge a human added, on both sides, asdetection: annotatedwith itsnote, and a human's note on an emitted edge as that edge'snote— andrelationship_annotations— the annotation file less the verdict entries of the edges it withholds. Underquery:table,ddl,values(per column: theentriesthe Markdown shows with counts and notes, the column'scoverage, the same coverage statement the Markdown renders, and for a sampled listshown_coverage— the share the shown entries cover; anumericortemporalcolumn's frequent values carryentries,coverage_statementandshown_coverageand nocoverage),joins(refers_toandreferenced_by, each edge as its columns, its table, itsdetection, anypath/target_path(referencer_pathon an incoming edge) and any humannote; both lists empty on a table with no edge, the key absent whenrelationships.yamlwas not read),nulls(<column>: <null_rate>),dictionary(column -> note),inherited_notesanddescription.null_rateis the raw ratestatistics.yamlwrites; it rides on eachvaluesblock and each single-columnrefers_toedge, andnullsholds every other nullable column, so a NOT NULL column appears in none. On a path-valued edgenull_rateis instead the derived rate of rows whose key reads NULL, rounded asnull_rateis, and absent when the print lists no part for that key; that edge's column keeps its own rate innulls.format: "yaml"-> the same structured object emitted as YAML.
Paged per §4.8, section by section in priority order, the table's identity first and its legend just below it: under profile the order the sections are listed above, and under query DDL, then the connection notes, then the value table, then the Joins list, then the data dictionary. A page holds whole sections until the next would cross the bound; a section larger than a page continues on the next at a row boundary — a Markdown line, or one column's entry in json/yaml — and a single row larger than a page at a character boundary in Markdown, or as parts (§4.8) in json/yaml.
format: "md"— every page but the last ends with the line<!-- next_cursor: <cursor> -->, preceded by a newline. Each page carries its own legend, defining only the terms of the lines it holds — after the identity on the first page, at the top of every later one, and none on a page that prints no term; with that line and each page's legend removed, the pages concatenate to the whole fragment less its legend.format: "json"— the cursor is thenext_cursorkey. The identity fields ride the first page; merging the pages key by key,statistics.columnsand the per-columnvalues,annotationsanddictionarymaps included, yields the whole object.format: "yaml"— each page is a YAML document of that page's json object, and every page but the last ends with the comment line# next_cursor: <cursor>.
format: "json" or "yaml" carries a _corrupted field naming every declared artifact (statistics, relationships, statistics_annotations, relationships_annotations), and manifest_annotations, that failed to parse, mapped to the parse-error message, or that is an annotation file not matching its schema (format spec §2.7), mapped to does not match its schema (SPEC 2.7): <address>: <message>; absent when nothing was corrupt. format: "md" prepends the same information as a note before the rendered sections. A corrupt artifact still degrades that one section rather than failing the call — this field is what tells a corrupt file from one the object's type never had.
format: "json" or "yaml" also carries a _missing field: an array naming every artifact kind the manifest declares for this table whose file is absent from disk. format: "md" states the same information as a Missing: ... line in the table's own header, distinct from _corrupted's prepended note — a declared-but-absent artifact and a declared-and-broken one are different gaps, and a caller needs to tell an artifact never declared from a declared one whose file is missing from one whose file does not parse. Absent when nothing declared is missing.
4.2 list_tables
Returns the FQNs of tables matching a pattern — the tool for which tables exist, their row counts, and whether each table's statistics are stale.
{ "name": "list_tables", "inputSchema": { "type": "object", "additionalProperties": false, "properties": { "connection": { "type": "string", "description": "Which database to read, by connection name; needed only when the server serves several, and the error for a missing one lists them. Omit it for the server's default" }, "pattern": { "type": "string", "minLength": 1, "description": "fnmatch glob over dotted table names, e.g. 'sales.*'; defaults to '*'" }, "detail": { "type": "boolean", "default": false, "description": "Project each entry's type/row_count/columns/profiled_at from the print's index plus its freshness verdict; false returns bare FQN strings, unchanged" }, "cursor": { "type": "string", "minLength": 1, "description": "`next_cursor` from this call's previous page; omit for the first page" } } }}Return: { "tables": ["arboretum.seedbank.accession", "arboretum.seedbank.germination_trial", ...], "total": 2 }. Sorted lexicographically; deterministic across calls. Paged per §4.8: tables is the paged list, and total counts every table pattern matched.
A failed_tables key beside tables lists, sorted and filtered by the same pattern, the tables the last generate run could not profile (SPEC 2.5); it is present only when non-empty, on the first page. A name listed there and absent from tables exists and is unprofiled.
detail: true returns { "tables": [{ "table": ..., "type": ..., "row_count": ..., "columns": ..., "profiled_at": ... }, ...] } instead — the manifest entry's own fields, projected alongside the FQN, still sorted lexicographically by FQN. row_count is absent for a plain view, the same as in the manifest itself.
Each detailed entry also carries the freshness verdict dbprint list and dbprint check reach for that table, from the same per-table threshold: freshness is live when profiled_at is within max_age_days, stale when it is older, and dormant when profiled_at is absent or unparseable; age_days is the age in days at the call, rounded to two places, and null for dormant; max_age_days is the threshold judged against — the entry's own recorded value, else what the connection's rules resolve to. A table whose threshold is refused (its rules raise, or its recorded max_age_days is negative or not whole) carries threshold_error with the reason dbprint check reports, and no freshness, age_days or max_age_days; the connection default is never substituted. When a threshold resolved without a min_rows rule that selects the table — no row count is available offline — the page carries warnings, one sentence naming those of its own tables. now is the server's clock at the call.
4.3 search_columns
The first call for locating a fact across the print when its table is not yet known — a name glob, a text search over column notes, and optional classification/sql_type/sensitivity/looks_like/redacted glob filters and a candidate_key match, ANDed. pattern alone reproduces the original by-name search; any predicate may be used alone or combined with the rest. A column's parts (SPEC 2.2.18) are searched too: pattern and text read a part as <column><path> (items[*].sku), the filters read the part's own fields, and a part's match carries part and occurrences beside column — so sensitivity: "*" also finds a sensitive key inside a document. Every glob filter needs the field present to match at all — sensitivity: "*" finds every column carrying any detection, never a column with none. A view described without a query carries a catalog-only statistics.yaml (SPEC 2.2.15), so pattern, classification and sql_type reach every column its catalog read; sensitivity, looks_like, redacted and candidate_key never match a catalog-only view's column, since the marker forbids every field those filters test.
{ "name": "search_columns", "inputSchema": { "type": "object", "additionalProperties": false, "properties": { "pattern": { "type": "string", "minLength": 1, "description": "fnmatch glob over column names, and over a part as `<column><path>` (`items[*].sku`); optional - omit to filter by the other predicates alone" }, "classification": { "type": "string", "description": "fnmatch glob against the column's classification (binary, boolean, categorical, composite, foreign_key_candidate, json, numeric, spatial, temporal, text, unsupported, vector)" }, "sql_type": { "type": "string", "description": "fnmatch glob against the column's sql_type" }, "sensitivity": { "type": "string", "description": "fnmatch glob against inferred.sensitivity (contact, credential, date_of_birth, demographic, employment, financial_account, geolocation, health, national_id, online_identifier, personal_name, postal_address) - a glob of '*' sweeps every column carrying any detection. A detection, never a verdict; its absence on a column is not an assertion that the column is safe" }, "looks_like": { "type": "string", "description": "fnmatch glob against inferred.looks_like (base64, bic, card_number, content_type, country_code, currency_code, ean, email, filename, gif, gzip, hex, iban, imei, ip, isbn, iso8601_date, iso8601_datetime, iso8601_duration, jpeg, json, jwt, latlon, mac_address, numeric_string, path, pdf, phone, png, postal_code, prose, semver, timezone, url, urn, uuid, vin, zip)" }, "redacted": { "type": "string", "description": "fnmatch glob against the column's redacted marker (drop, hash, mask)" }, "candidate_key": { "type": "boolean", "description": "Exact match against inferred.candidate_key" }, "text": { "type": "string", "minLength": 1, "description": "Case-insensitive substring matched against the column name, the note people wrote on it and its per-value notes - words, not meaning. A match found through per-value notes carries them as `value_notes`" }, "connection": { "type": "string", "description": "Which database to read, by connection name; needed only when the server serves several, and the error for a missing one lists them. Omit it for the server's default" }, "cursor": { "type": "string", "minLength": 1, "description": "`next_cursor` from this call's previous page; omit for the first page" } } }}Return:
{ "matches": [ { "table": "arboretum.seedbank.accession", "column": "email", "sql_type": "VARCHAR(320)", "classification": "text", "row_count": 1000, "rows_scanned": 250, "looks_like": "email", "sampled": 250, "matched": 248, "sensitivity": "contact", "redacted": "hash", "candidate_key": true, "annotation": "Always lowercased on write." } ], "total": 1}annotation is present only when the column has one in statistics.annotations.yaml; sql_type and classification are empty strings for a column known only through its annotation (a view with no statistics.yaml). Matches are ordered by (table FQN, column name) lexicographic.
row_count and rows_scanned are present whenever the manifest/column carry them — rows_scanned only when the table's file carries a scope block (SPEC 2.2.8). A match on a table read in part, a part's included, also carries that scope block verbatim: the block governs, so a column missing its rows_scanned echo still reads as scanned-set, and sample versus filter says whether a count may be rescaled. looks_like/sampled/matched are included together, only on a column carrying a looks_like verdict (SPEC 4.1.3) — a verdict drawn from two values reads identically to one drawn from ten thousand without the draw size beside it. sensitivity, redacted and candidate_key (with candidate_key_exception when the ratio falls short of 1.0) are included the same way: only on a column carrying the field, so a caller filtering on any of these four sees the matched category, not just the column name.
text is a case-insensitive substring match against the column name, the column's note in statistics.annotations.yaml and each of its per-value notes (SPEC 2.7.1) — words, not meaning; table-level description.md is not searched. It is ANDed with every other filter. A match found through per-value notes carries value_notes, the list of {value, note} entries whose note contains the text; a match through the name or the column note alone carries none.
Paged per §4.8: matches is the paged list, walked in table FQN order and column name order within a table, and total counts every match. A glob matching no real value (e.g. classification: "nope") returns an empty match list, never an error — the enumerated values in each filter's description are the format's own, not a validated allowlist.
unreadable_tables is present only when at least one table's statistics.yaml failed to parse or its statistics.annotations.yaml failed to parse or does not match its schema (format spec §2.7), naming every such table's FQN, sorted, on the first page whichever page that table's columns would have reached. A statistics.yaml failure means that table's columns are absent from matches entirely, since the column list itself could not be read; a statistics.annotations.yaml failure alone still returns the table's columns, only without the annotation field an annotation would have carried. Either way a corrupt file removes only what it alone supplied, not the whole call, and the reply always names it.
switched_off_tables is present only when looks_like is among the filters and at least one walked table's statistics.yaml names looks_like in disabled (SPEC 2.2.21), naming every such FQN, sorted, on the first page. Those tables were never sampled for a value shape, so a column of theirs missing from matches is not a finding that it holds no such value.
4.4 get_manifest
Returns the parsed manifest.yaml content as a JSON object — an index of tables and their artifacts, not a semantic catalogue of what they mean. For table names, row counts and freshness, list_tables with detail: true is the shorter answer.
{ "name": "get_manifest", "inputSchema": { "type": "object", "additionalProperties": false, "properties": { "connection": { "type": "string", "description": "Which database to read, by connection name; needed only when the server serves several, and the error for a missing one lists them. Omit it for the server's default" }, "pattern": { "type": "string", "minLength": 1, "description": "fnmatch glob over the FQN keys of `tables`, the same spelling `list_tables` takes; filters that map and `failed_tables` only" }, "cursor": { "type": "string", "minLength": 1, "description": "`next_cursor` from this call's previous page; omit for the first page" } } }}Return: the parsed manifest dict per SPEC §2.5.
The tables map is paged per §4.8, in FQN order; every other key of the document rides the first page whole. pattern filters that map, and failed_tables with it, by the same fnmatch spelling list_tables takes, so one spelling means one thing across the surface. Every page carries total, the number of tables entries pattern matched. A reply that dropped a header key to save bytes would be a different artifact, not a paged one.
4.5 get_diff
Returns the parsed diff.yaml content as a JSON object — the answer to what changed between the last two runs: structural changes, relationship changes and statistics that drifted. A table's current state, rather than what changed, comes from get_table_context.
{ "name": "get_diff", "inputSchema": { "type": "object", "additionalProperties": false, "properties": { "connection": { "type": "string", "description": "Which database to read, by connection name; needed only when the server serves several, and the error for a missing one lists them. Omit it for the server's default" }, "table": { "type": "string", "minLength": 1, "description": "Keep only changes naming this fully-qualified table, including the relationship events that name it as source or target" }, "kind": { "type": "string", "enum": ["table_added", "table_removed", "table_type_changed", "external_changed", "column_added", "column_removed", "column_type_changed", "column_nullable_changed", "column_default_changed", "column_physical_name_changed", "column_collation_changed", "statistic_changed", "table_row_count_changed", "grain_changed", "physical_layout_changed", "merging_changed", "depends_on_changed", "relationship_added", "relationship_removed", "relationship_modified", "index_added", "index_removed", "index_modified", "comment_changed"], "description": "Keep only changes of this kind" }, "cursor": { "type": "string", "minLength": 1, "description": "`next_cursor` from this call's previous page; omit for the first page" } } }}Return: the parsed diff dict per SPEC §2.6. The diff is the one produced by the last successful dbprint generate for the connection.
The changes list is paged per §4.8, in file order; every other key of the document rides the first page whole. Two filters narrow it, ANDed: table keeps the events naming that fully-qualified table — including the three relationship events, which carry source_table and target_table where every other event carries table, so a filter reading one field alone would silently drop them — and kind keeps one event kind, declared as the packaged diff.schema.json enum so the list cannot drift from the format. Every page carries total, the number of events the filters kept.
The schema guarantees none of those fields. $defs/Change requires kind alone and no variant declares a table field, so these are producer facts: an event carrying none of the three is simply not matched by a table filter.
A relationship event about an edge a human rejected is withheld before either filter runs: one whose source_column, target_table and target_column match a verdict: rejected entry in the source table's relationships.annotations.yaml, unless that table's current relationships.yaml declares the edge. summary.relationships_changed counts the events returned; diff.yaml on disk is unchanged.
4.6 get_reference
Returns a slice of the format spec or the assertion DSL spec, addressed by section number, or of the packaged reading guide, addressed by heading text. The guide is the installed dbprint version's own copy, not a print's reading.md, which the dbprint://<connection>/reading resource serves. Depends on no connection and no print; connection is not a parameter. What one print's tables hold comes from get_table_context and list_tables.
{ "name": "get_reference", "inputSchema": { "type": "object", "additionalProperties": false, "properties": { "document": { "type": "string", "enum": ["assertions", "guide", "spec"], "description": "Which document - the format spec, the assertion DSL, or the reading guide (how to read each json/yaml field)" }, "section": { "type": "string", "minLength": 1, "description": "A section number in the document's own scheme (e.g. '3', '2.2.4'), or a spec_ref citation copied verbatim from a finding ('\u00a72.2.4', 'ASSERTIONS.md \u00a71.4') - any heading depth. Omit for the table of contents. For guide, a heading's text (e.g. 'Vocabulary'), matched case-insensitively." }, "cursor": { "type": "string", "minLength": 1, "description": "`next_cursor` from this call's previous page; omit for the first page" } }, "required": ["document"] }}Return: a markdown string, paged per §4.8 the way get_table_context's Markdown is.
- With
section, the matching heading and its own body — everything up to its first subsection, or up to the next heading at the same or a shallower level when it has none. A section with subsections then lists its direct ones, one- <number and title>line each, underSubsections, each read by its own number:; their text is read by passing that number. One rule for every depth,##/###/####alike. - Without
section, the document's heading tree instead of the whole document. sectionMUST accept the document's own bare numbering scheme ("3","2.2.4") AND aspec_refcitation copied verbatim from a finding ("§2.2.4","ASSERTIONS.md §1.4", the spelling every finding'sspec_refuses) — a caller never strips the citation prefix by hand.sectionnaming a number no heading in that document carries MUST fail (isError: true, §8.2), detail naming the section numbers that document actually has.- For
guide,sectionis a heading's text, matched case-insensitively with whitespace collapsed; the guide's headings carry no numbers. A heading with subsections lists its direct ones underSubsections, each read by its own heading:, each read by passing its title. A text no heading carries MUST fail, detail naming the guide's headings.
document is a closed enum of exactly three values; a fourth document is a decision for a future revision of this spec, not an extension point a server infers.
4.7 resolve_value
Resolves a phrase, a code or a spelling against one column's published values — what a caller reads before writing a literal into a filter. Given part, it resolves against that part's values instead (SPEC 2.2.18), and the reply carries part; a part's occurrences are counted in the scanned rows, so on a table read in part its reply follows the scoped paragraph below exactly as its column's does. Offline: the column's values list, its counts, and any values[].note in statistics.annotations.yaml. No database, and no second artifact.
{ "name": "resolve_value", "inputSchema": { "type": "object", "additionalProperties": false, "properties": { "table": { "type": "string", "minLength": 1, "description": "Fully-qualified table name" }, "column": { "type": "string", "minLength": 1, "description": "Column name as the print spells it" }, "part": { "type": "string", "minLength": 1, "description": "A part of the column, by its path as the print keys it (`.status`, `[*]`); omit it to resolve against the column itself" }, "text": { "type": "string", "minLength": 1, "description": "The phrase, code or spelling from the question, as written" }, "connection": { "type": "string", "description": "Which database to read, by connection name; needed only when the server serves several, and the error for a missing one lists them. Omit it for the server's default" }, "cursor": { "type": "string", "minLength": 1, "description": "`next_cursor` from this call's previous page; omit for the first page" } }, "required": ["table", "column", "text"] }}The reply carries table, column, text and a match, which is the first of these that applies:
match | Meaning | Carries |
|---|---|---|
stored | text is a listed value, or folds to one under the normalization key (SPEC 2.2.4: trimmed, lower-cased) | spellings — every listed spelling that folds to it, each with its count and its note. Several means one category stored several ways: a predicate needs all of them |
definition | text names what a value's note says it means, matched on word boundaries either way round | candidates — every value whose note matches, with counts and notes |
nearest | Neither, but listed values resemble text | candidates — up to five, ranked by trigram similarity above a floor, each carrying its score |
none | Nothing resembled it | — |
unavailable | The column publishes no value list for its classification, its list was not measured this run (the reason says which), its values are redacted, or its table was read in part and the scan read no rows | reason |
Every reply carries coverage (the column's values_coverage, null where the classification publishes none), exhaustive (whether the list carries every distinct value the column has over the table — values_coverage of 1.0, or on a numeric/temporal column frequencies.listed equal to an exact cardinality, SPEC 2.2.5, and never on a table whose file carries scope) and listed (how many entries the print carries). Where the list is a top-N cut the reply also carries sample_caveat, a fixed sentence stating that a spelling absent from a sample is not evidence it is absent from the column. Where the list is complete over what was read and at most fifty entries long, the reply carries domain: the whole list with counts and notes, so a small vocabulary needs exactly one call.
Paged per §4.8: every key other than the lists rides every page, spellings or candidates is walked before domain, and total counts the entries of both. A value is never shortened — one too long for a page arrives in parts.
On a table read in part (SPEC 2.2.8) every reply, unavailable included, also carries the file's scope block and its row_count; exhaustive is false; a complete list's sample_caveat states that the list is the whole domain over the rows scanned, not over the table; and domain is the scanned rows' whole domain. none describes the listed values only — whether that extends to the column is what exhaustive says.
A match: "none" is an answer, not an error. An empty exhaustive list — a column with no non-null value publishes values: [] at values_coverage: 1.0 (SPEC 7.4) — answers none with an empty domain. An unknown table fails per §8.2 as get_table_context does; an unknown column fails the same way, its detail naming the columns that table's statistics do carry. A statistics.yaml the manifest declares but that is absent from disk, or that does not parse, fails with the same errors §8.2 lists for those conditions — never as an unknown column; a statistics.annotations.yaml that does not parse, or does not match its schema (format spec §2.7), fails the same way, since the notes it would carry are part of the answer. A table that declares no statistics artifact answers match: "unavailable" with that reason.
4.8 Paging
Every reply is bounded: the text a client receives for one call — a dict reply serialized as JSON with two-space indent, or a bare string as it stands — is at most 20,000 characters. A reply that would be longer is cut into pages; nothing is dropped to fit. A list reply pages its items as below; a document — get_table_context, get_reference — pages its text as §4.1 states. The bound is the server's, not the caller's — a caller wanting less narrows with the tool's own filters.
cursor— an optional string argument. Omitted, a call returns its first page.next_cursor— present on every page but the last. Passed back unchanged ascursor, with every other argument exactly as before, it returns the next page.total— on every page of a list reply: how many items the call matched in all, never how many this page holds.- The first page carries the rest. Every key other than the paged list — a document's header fields,
failed_tables,unreadable_tables— comes before any list item and appears once, on the first page, except where a tool's own section says a key rides every page (§4.7). A key whose value alone exceeds a page is sent in parts like an item, starting on the first page, so the list begins only after it. - Order. Pages follow the list's own order, so following
next_cursorto the last page yields each item exactly once. - Parts. An item too large for any page is sent in its own place as consecutive entries
{"part": <n>, "parts": <m>, "text": "<chunk>"}, one per page. Thetextof parts1tom, concatenated in order, is the item's JSON, and parses to the item.
A cursor is opaque and belongs to the call that issued it: the tool, its other arguments, and every print file its reply read. Following a cursor after any of those files changed on disk is a failed call (isError: true, §8.2) saying the print changed, never a page that silently skips or repeats items — the call starts over without cursor. A cursor that does not decode, or one issued by another tool or under other arguments, is a failed call too.
5. Multi-connection model
5.1 Default connection resolution
dbprint serve [CONNECTION] resolves the served connection set and default at startup:
| Invocation | Served connections | Default for tool calls |
|---|---|---|
dbprint serve with single connection in .dbprint.yaml | The single connection | The single connection |
dbprint serve with auto: true set on exactly one of >= 2 connections | That connection | That connection |
dbprint serve with auto: true set on >= 2 connections | Every auto: true connection | None — tool calls without connection? MUST return an error |
dbprint serve <name> | <name> | <name> |
dbprint serve with no auto: true and >= 2 connections | (rejected at startup) | Server exits code 1 with hint listing valid names |
5.2 connection? parameter on tool calls
Every tool accepts an optional connection parameter:
- When provided, the tool MUST evaluate against that connection. If the connection is not in the served set, the tool MUST return
-32602 InvalidParams— naming it as configured-but-unserved when.dbprint.yamldeclares it and this instance simply was not started with it, or as unconfigured when.dbprint.yamldoes not declare it at all (§8.1/8.2). - When omitted, the tool MUST use the default connection. When no default exists (multi-connection serve without
auto), the tool MUST return-32602 InvalidParamswith detail listing valid connection names.
5.3 Resource URIs always carry the connection
Resource URIs include the connection as the host segment (dbprint://<connection>/...). Resource enumeration and reads are always unambiguous regardless of how many connections the server exposes.
6. Transport
6.1 stdio (default)
Default transport for editor/agent integration. The server reads MCP frames from stdin and writes responses to stdout. Logging and warnings MUST be routed to stderr; mixing them into stdout corrupts the protocol.
Selected with --transport stdio (or by omitting the --transport flag).
6.2 HTTP / SSE
Local-only HTTP transport for clients that prefer network sockets over stdio.
| Flag | Default | Effect |
|---|---|---|
--transport http | (off) | Switches to HTTP / SSE |
--host HOST | 127.0.0.1 | Listen address; must be loopback |
--port PORT | (required when --transport http) | TCP port |
The server MUST bind only to loopback addresses (127.0.0.1 or ::1). Binding to other interfaces (0.0.0.0, public IPs) MUST be rejected at startup with a clear error.
The server answers only a request whose Host is 127.0.0.1:<port>, localhost:<port> or [::1]:<port> for the port it is bound to, and whose Origin, when present, is one of those as an http:// origin; any other Host gets 421 and any other Origin 403. A web page that rebinds its own name to a loopback address therefore cannot reach it.
There is no authentication, and no remote deployment model.
7. Lifecycle
7.1 File freshness
The server MAY keep a parsed print file in memory between calls, provided every tool call and every resources/read call checks each file it reads against that file's identity on disk — its modification time, size and inode — and parses it again when any of them differs. A file that fails to parse is never kept, so a corrected file is read on the next call.
Consequence: a dbprint generate run completing while the server is running is visible to the next tool call. No restart required.
7.2 Capabilities
Per §2, the server advertises subscribe: false and listChanged: false. The server MUST NOT emit notifications/resources/updated or notifications/resources/listChanged.
resources/templates/list returns one template per §3.2 URI pattern in its page form — dbprint://{connection}/{table}/statistics{?page,version}, dbprint:///reference/{document}{?page,version} and so on — so a client that builds URIs from templates reaches every page.
7.3 Shutdown
The server MUST handle SIGTERM and SIGINT cleanly:
- Stop accepting new requests.
- Allow in-flight requests to complete.
- Flush stderr.
- Exit with code 0.
For stdio transport, EOF on stdin is equivalent to SIGTERM — the server MUST exit cleanly when the client closes the connection.
8. Error model
Two channels, not one. resources/read failures are genuine JSON-RPC protocol errors — the codes below reach the wire in the response's error field. tools/call failures are not: a tool fault is a successful RPC carrying isError: true, with the detail string as the result's text content, per the MCP tool-call idiom, and no JSON-RPC code is sent for one. Detail strings are intended for LLM consumers either way — verbose, structured, and actionable.
8.1 Resource errors (resources/read) — JSON-RPC protocol errors
| Trigger | JSON-RPC code | Detail format example |
|---|---|---|
Reading an absent optional file (e.g., missing description.md / statistics.annotations.yaml) | -32602 InvalidParams | "description.md is optional and not authored for table 'arboretum.seedbank.accession'." |
| Reading a resource for an unconfigured connection | -32602 InvalidParams | "connection 'bar' not in .dbprint.yaml. Configured: ['a', 'b', 'c']" |
| Reading a resource for a connection configured but not served by this server instance | -32602 InvalidParams | "connection 'staging' is configured but not served by this instance. Served: ['arboretum']. Restart the server naming it to serve it." |
| Reading a resource for an unknown table | -32602 InvalidParams | "table 'foo' not found in connection 'bar'. Call list_tables with connection 'bar' for valid names." |
| Reading a resource for a table the last run could not profile | -32602 InvalidParams | "the last generate run could not profile 'arboretum.seedbank.extra'; run dbprint generate to see the cause" |
Reading a kind the manifest never declares for that table (e.g. relationships for a plain view declaring none) | -32602 InvalidParams | "relationships is not declared for table 'arboretum.seedbank.v' - check the object's type before requesting it; the manifest does not declare every kind for every object." |
| Manifest references a file that is absent on disk | -32603 InternalError | "manifest references statistics.yaml but file is absent at <path>. Re-run dbprint generate." |
| Manifest parses as YAML but is not the shape every reader below it walks | -32603 InternalError | "<path>: <reason the shape is wrong>" |
| YAML parse error in a print file (any YAML-typed artifact, not manifest.yaml alone) | -32603 InternalError | "<path>: YAML parse error: <message>" |
Reading diff with no committed diff for the connection | -32603 InternalError | "no diff available at <path>. Run dbprint diff or dbprint generate first." |
Reading reading with no reading.md for the connection | -32603 InternalError | "no reading guide available at <path>. Run dbprint generate first." |
| Resource requested with malformed URI | -32602 InvalidParams | "URI 'foo' does not match the dbprint:// scheme." |
page outside the resource's pages, or not a whole number | -32602 InvalidParams | "'dbprint://a/manifest?page=9' names no page of this resource: pages run 1 to 3." |
A page past the first without version | -32602 InvalidParams | "'dbprint://a/manifest?page=2' asks for a page past the first without `version`. Pass the `version` the first page's `_meta` reported, as `?page=<n>&version=<v>`." |
version the file has moved past | -32602 InvalidParams | "'dbprint://a/manifest?page=2&version=0f1e': the file changed on disk since that version was read. Read page 1 again for its current version." |
resources/list cursor over a print that changed since it was issued | -32602 InvalidParams | "the print changed on disk since this cursor was issued, so resources/list cannot resume where it left off. Call again without `cursor` to start over." |
8.2 Tool errors (tools/call) — isError: true, not a protocol error
Every call is checked against the tool's own advertised inputSchema before the tool body sees it, so an argument fault names the key, the value that arrived, and what the tool accepts. Two arguments fold case before that check — get_table_context's format and purpose — so MD and QUERY are accepted; get_reference's document does not fold. A non-string value is a type fault, never an enum one.
| Trigger | Detail format example |
|---|---|
Unknown table | "table 'foo' not found in connection 'bar'. Call list_tables with connection 'bar' for valid names." |
A table the last run could not profile | "the last generate run could not profile 'arboretum.seedbank.extra'; run dbprint generate to see the cause" |
Unknown connection | "connection 'bar' not in .dbprint.yaml. Configured: ['a', 'b', 'c']" |
connection configured but not served by this server instance | "connection 'staging' is configured but not served by this instance. Served: ['arboretum']. Restart the server naming it to serve it." |
| A key the tool does not declare | "list_tables takes no argument 'schema'. Accepted: ['connection', 'detail', 'pattern']." |
| A required key absent | "get_table_context requires 'table'." |
| A key of the wrong type | "pattern 7 must be of type string." |
| An empty string where one is required | "table '' must be a non-empty string." |
| A value outside a declared enum | "format 'yml' must be one of ['md', 'json', 'yaml']." |
resolve_value called with a column the table's statistics do not carry | "column 'rnk' not found in table 'arboretum.seedbank.taxon'. Columns: rank, scientific_name, taxon_id." |
resolve_value on a table whose declared statistics.yaml is absent from disk | "manifest references statistics.yaml but file is absent at <path>. Re-run dbprint generate." |
resolve_value on a table whose statistics.yaml or statistics.annotations.yaml does not parse | "<path>: YAML parse error: <message>" |
resolve_value on a table whose statistics.annotations.yaml does not match its schema (format spec §2.7) | "<path>: does not match its schema (SPEC 2.7): <address>: <message>" |
No connection given when no default exists | "no default connection; pass connection explicitly. Configured: ['a', 'b', 'c']" |
| Manifest references a file that is absent on disk | "manifest references statistics.yaml but file is absent at <path>. Re-run dbprint generate." |
get_diff with no committed diff for the connection | "no diff available at <path>. Run dbprint diff or dbprint generate first." |
| Manifest parses as YAML but is not the shape every reader below it walks | "<path>: <reason the shape is wrong>" |
| YAML parse error in a print file | "<path>: YAML parse error: <message>" |
| Annotation file parses but does not match its schema (format spec §2.7) | "<path>: does not match its schema (SPEC 2.7): <address>: <message>" |
| Tool requested with unknown name | "unknown tool 'foo'. Available: ['get_diff', 'get_manifest', ...]" |
cursor that no reply issued | "cursor 'x' is not one this server issued. Pass back a reply's `next_cursor` unchanged, or call again without `cursor` for the first page." |
cursor issued by another tool, or by the same tool under other arguments | "this cursor was not issued by list_tables with these arguments. Repeat the call that returned it with the same arguments, or call again without `cursor`." |
cursor whose print files changed on disk since it was issued | "the print changed on disk since this cursor was issued, so list_tables cannot resume where it left off. Call again without `cursor` to start over." |
get_reference called with document outside its declared enum | "document 'readme' must be one of ['assertions', 'guide', 'spec']." |
get_reference called with a section no heading in that document carries | "section '9.9' not found in spec. Available: ['0', '0.1', '0.2', ...]" |
get_reference called with document: guide and a section no guide heading carries | "section 'No such heading' not found in guide. Available: ['Fields that are easy to misread', ...]" |
8.3 Error responses are not exceptions
Neither channel crashes the connection. The server MUST NOT terminate on an application-level error, in either form. Crashes are reserved for genuine internal-state corruption that prevents serving the next request safely.
9. Forward compatibility
Consumers MUST tolerate:
- Unknown tool names — clients SHOULD ignore unknown tool entries in
tools/listrather than treating them as protocol violations. - Unknown resource URIs matching the
dbprint://scheme but with new patterns — clients SHOULD accept entries returned byresources/listwithout enumerating expected patterns. - New capability flags added under
resourcesortools— clients SHOULD ignore unknown flags. - New mimeTypes on resources — clients SHOULD treat unknown mimeTypes as opaque text.
- New optional fields in tool return shapes — clients SHOULD ignore unknown fields.
The tool set and resource URI patterns MAY grow in MINOR releases (additive only). Existing tool signatures and resource URI patterns MUST NOT change within a MAJOR version.
Cross-references
format/v1/SPEC.md— format specification formanifest.yaml,diff.yaml, and per-table artifactsASSERTIONS.md— assertion DSL specification fordbprint check --online