Skip to content

refactor(mcp): scope semver to the MCP tool surface and hide library modules - #301

Merged
StefanSteiner merged 1 commit into
tableau:mainfrom
StefanSteiner:refactor/mcp-library-not-public-api
Sep 7, 2026
Merged

refactor(mcp): scope semver to the MCP tool surface and hide library modules#301
StefanSteiner merged 1 commit into
tableau:mainfrom
StefanSteiner:refactor/mcp-library-not-public-api

Conversation

@StefanSteiner

Copy link
Copy Markdown
Contributor

Problem

hyperdb-mcp made two contradictory statements about what it guarantees.

hyperdb-mcp/src/lib.rs exported 21 public modules while carrying a lint reason stating the library target "is not a documented API surface". But hyperdb-mcp/README.md promised, without scope, that "As of 1.0.0 the public API is stable and follows semantic versioning, so breaking changes require a major release."

Against a crate where every module is pub, that sentence promised semver stability on all 21 modules and every item inside them. It has already cost twice: #289 had to record DaemonState gaining a private field as a source-breaking change, and #295 had to document state_perms becoming new public surface — both incidental internals that only counted as API because of the blanket promise.

Removing or hiding public surface is free before 1.0.0 and needs a major version afterwards, so this lands now. The library is demonstrably unconsumed: hyperdb-mcp has zero reverse dependencies on crates.io (reverse_dependencies returns "total": 0), the newest published version is 1.0.0-rc.2 (max_stable is 0.7.3, so 1.0.0 is not out), and no workspace crate depends on it.

What changed

1. The README promise is scoped — the load-bearing change. The stable, semver-governed surface is now the MCP tool surface: tool names, parameters, and behavior as reached over the MCP protocol. The Rust library target is explicitly excluded. This is what actually stops incidental internals from counting as breaking changes. The AI-assisted note and its tone are preserved; the paragraph is wrapped only to satisfy MD013.

2. All 21 modules are #[doc(hidden)]. Applied uniformly rather than to daemon alone — one hidden module beside twenty visible ones would leave the contradiction in place and imply the other twenty are supported.

3. Nothing was privatised, and nothing could be. pub(crate) is unavailable for every one of the 21 modules. Cargo compiles the [[bin]], each file under tests/, and examples/demo.rs as separate crates that reach library items only through the external hyperdb_mcp:: path:

Reached from Modules
binary only paths
binary + tests daemon, diagnostics, engine, server
binary + tests + example engine
tests + example chart, ingest
tests only attach, error, export, ingest_arrow, inspect, lakehouse, readme, saved_queries, schema, stats, subscriptions, table_catalog, version, watcher

So pub is load-bearing for compilation, not an API commitment.

What #[doc(hidden)] does and does not achieve: it removes the modules from published rustdoc and signals intent, but the items remain technically reachable. The README scoping in (1) is what actually defines the compatibility promise.

4. version, error and readme are hidden too — no exceptions. Each was considered and rejected:

  • error — the stable contract is the serialized SCREAMING_SNAKE_CASE code (HYPERD_NOT_FOUND) an LLM pattern-matches, which the tool surface already covers. ErrorCode is not #[non_exhaustive], so leaving it visible would make every new error code a source-breaking change — exactly the trap being removed.
  • version — its strings reach users through the status tool and the hyper://workspace resource. The tool output is the promise; the Rust helpers are how it is built.
  • readme — exists to serve get_readme. What is stable is what that tool returns, which hiding the const does not affect.

With zero reverse dependencies there is no consumer whose needs justify a carve-out, and any carve-out would re-create the contradiction in narrower form.

5. Root README.md aligned. Its Project Status note claimed a repo-wide "the public API is stable" that could be read as covering this crate's library. It now says the definition is per-crate and points at the crate table, whose hyperdb-mcp row gained a scoping clause — mirroring the note already present for hyperdb-api-core.

6. Architecture bullets use code spans, not intra-doc links. Verified against real rustdoc output: a link to a #[doc(hidden)] item is not resolved and renders as literal [<code>engine</code>] brackets, while `error` renders cleanly. readme_tests.rs:425 locates the engine bullet by those delimiters, so they moved with it. The terminology it guards is unchanged, and red-before-green confirmed it still fails when the wording is removed.

Why no ! / **BREAKING:** marker

Nothing is privatised, and #[doc(hidden)] has no effect on name resolution — no code that compiled before stops compiling. Per this changelog's own convention the marker denotes source-breaking changes; using it here would misuse it and wrongly imply a major bump on a pre-1.0.0 line. The changelog entry says so explicitly and notes that narrowing a module to pub(crate) later would carry it.

Verification

Isolated CARGO_TARGET_DIR; HYPERD_PATH="$PWD/.hyperd/current" (the pinned 0.0.26479 engine, main.0.0.26479.r96880f6a); HYPERDB_STATE_DIR pinned to a temp dir.

Gate Result
cargo build -p hyperdb-mcp --bins exit 0
cargo test -p hyperdb-mcp --no-run exit 0, 35 test binaries linked
cargo build -p hyperdb-mcp --examples exit 0
cargo fmt --all -- --check exit 0, 0 diff lines
cargo clippy --workspace --all-targets --all-features -- -D warnings exit 0, 0 warnings
cargo test -p hyperdb-mcp exit 0, 663 passed / 0 failed / 17 ignored across 36 binaries
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps exit 0, 0 warnings
npx markdownlint-cli2 exit 0, 0 issues in 68 files

Hidden modules confirmed absent from rustdoc, by regenerating docs from the pristine lib.rs and from this branch in the same target dir:

module doc dirs id="modules" sections sidebar
before 21 1 populated
after 0 0 window.SIDEBAR_ITEMS = {};

doc/hyperdb_mcp/ now contains only index.html, all.html, and sidebar-items.js.

…modules

`hyperdb-mcp/src/lib.rs` carried a lint `reason` saying the library target "is
not a documented API surface", while `hyperdb-mcp/README.md` promised, without
scope, that "the public API is stable and follows semantic versioning". Against
a crate where all 21 modules are `pub`, that sentence promised semver stability
on every module and every item inside them, which is why two incidental
internals had to be written up as API events: `DaemonState` gaining a private
field (tableau#289) and `state_perms` becoming new public surface (tableau#295).

Scope the promise to the MCP tool surface — tool names, parameters, and
behavior as reached over the MCP protocol — and state that the Rust library
target is excluded. The root README says the same in its crate table, next to
the equivalent note that already existed for `hyperdb-api-core`.

Mark all 21 modules `#[doc(hidden)]`. None can become `pub(crate)`: Cargo
compiles the `[[bin]]`, each file under `tests/`, and `examples/demo.rs` as
separate crates that reach library items only through the external
`hyperdb_mcp::` path, and every module is used by at least one of them
(`paths` by the binary alone; `stats`, `subscriptions` and `watcher` by one
test file each). So `pub` is load-bearing for compilation, not an API
commitment. Nothing is privatised and no code that compiled before stops
compiling, so this is not source-breaking.

The crate-level architecture bullets now name modules in plain code spans
instead of intra-doc links, because rustdoc declines to resolve a link to a
hidden item and renders it as literal `[engine]` brackets. `readme_tests.rs`
locates the `engine` bullet by that syntax, so its delimiters move with it;
the terminology it guards is unchanged and still fails when removed.
@StefanSteiner
StefanSteiner merged commit 8760df2 into tableau:main Sep 7, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant