Skip to content

hub-mcp Phase 4: Quarto-specific intelligence (bd-738mbhpj) - #797

Open
shikokuchuo wants to merge 9 commits into
braid/bd-3qe7unp7-hub-mcp-phase-3-collaborationfrom
braid/bd-738mbhpj-hub-mcp-phase-4-quarto-intelligence
Open

shikokuchuo wants to merge 9 commits into
braid/bd-3qe7unp7-hub-mcp-phase-3-collaborationfrom
braid/bd-738mbhpj-hub-mcp-phase-4-quarto-intelligence

Conversation

@shikokuchuo

Copy link
Copy Markdown
Member

Phase 4 of the hub-mcp elevation plan (bd-738mbhpj), stacked on #794. The MCP server gains Quarto-specific tools: structural qmd editing, rendering, and the embedded documentation. Design and full record: claude-notes/plans/2026-10-05-elevate-quarto-hub-mcp.md.

New tools

  • get_outline returns the heading tree of a .qmd file with 1-based line ranges (CAP-11).
  • read_file and patch_file take a section selector that reads or replaces exactly one section (CAP-11). A section patch touches only the section's lines, so a collaborator's edits elsewhere survive. The replaced range is exactly the range read_file shows, heading included. An empty new_string deletes the section.
  • render materializes the project to a temporary directory and runs q2 render --json-errors (CAP-12). It returns structured diagnostics with Q- codes and source locations. It removes the directory afterwards. The tool exists only with --allow-render, because rendering runs project code. It never appears with --read-only.
  • docs answers Quarto usage questions from the documentation embedded in the q2 binary (CAP-13). query searches titles and full text. page returns one page's markdown. No project connection or network is needed.

CLI additions

  • q2 docs llms --list --json prints the page list as a JSON array (bd-b6cocsxw).
  • q2 docs llms --embed-info --json prints the embed provenance as JSON (bd-b6cocsxw).

Build and CI

  • wasm-qmd-parser was broken on main for every target. It builds again, and its local C shim is replaced by the shared wasm-c-shim crate.
  • The crate gains a nodejs build, staged into the MCP bundle like the keyring addon. The server and the web preview now use the same parser.
  • The q2 mcp launcher injects QUARTO_Q2_PATH, so the server can find the binary for renders and docs.
  • Release pipelines build the parser once and stage it on every leg. CI runs the parser test tier on the toolchain-rich job and skips it where no toolchain exists.

Decisions worth knowing

  • clear_capture stays human-only (Q-4). It mutates shared state that drives other collaborators' preview engines.
  • The tool surface is now at the 24-tool ceiling. Phase 5 additions must displace existing tools or move to resources and prompts.

Verification

  • cargo xtask verify: 14 of 14 legs pass. The MCP suite: 498 passed, 3 skipped.
  • Agent-task evals: 15 of 15 tasks pass, median 5 turns per task, no unintended tool errors.
  • Real-binary end-to-end recorded in the plan: outline, section read and replace, docs search, and a broken-include render that returned Q-17-2 with file, line, and column.

@posit-snyk-bot

posit-snyk-bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

✅ Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
✅ Open Source Security 0 0 0 0 0 issues
✅ Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

…1; bd-738mbhpj)

The crate no longer compiled on main for any target (build-wasm.yml is
workflow_dispatch-only, so the rot was invisible; CI gap filed bd-s0tm9sdq):

- tree-sitter 0.26 compiles tree-sitter-language's wasm/src/*.c into
  libtree_sitter.rlib, colliding with the crate's local c_shim.rs
  (duplicate strong symbols). Apply the established repo pattern the
  crate was missing: [patch.crates-io] tree-sitter-language ->
  tree-sitter-language-wasm-shim (empty C stubs; 2026-04-20
  wasm-shim-merge plan).
- The patch leaves snprintf undefined; the stale local c_shim.rs
  predated the merged superset. Delete it and take the shared
  wasm-c-shim crate (strict superset; same as wasm-quarto-hub-client
  and pampa's wasm tests).
- The workspace root's panic=unwind rustflags leak in via cargo's
  config-array concatenation and break the link without build-std.
  Trailing -C panic=abort in the crate's .cargo/config.toml (rustc
  last-wins) keeps this crate Lua-free and rust-src-free.

Spike S-1 decision recorded in the plan: CAP-11's parser is
wasm-qmd-parser --target nodejs staged as an external package in the
hub-mcp dist-bundle (keyring pattern), not a napi-rs module. Release
wasm 4.61 MB raw / 1.24 MB gzip; init 3.8 ms; warm parse 0.11 ms
(1 KB) / 28 ms (34 KB). napi-rs rejected: new per-platform build
axis, size multiplies per target, no latency win at MCP scales.

Verified: wasm-pack build --target nodejs and --target web (dev +
release) with committed config alone; Node require/ESM named imports
work; parse_qmd returns {success, ast:<json>} with byte-offset
locations. Full workspace suite green (15487/15487).
…(CAP-11; bd-738mbhpj)

Structural reads and edits for qmd files, on the S-1 spike's parser
(wasm-qmd-parser --target nodejs, staged into dist-bundle as an
external package — the keyring pattern):

- get_outline: the heading tree of a .qmd file with level/title/id and
  1-based line ranges (section ends before the next same-or-higher
  heading). Read-only surface.
- read_file gains a section selector returning exactly one section
  (heading included); mutually exclusive with offset/limit; max_bytes
  still caps. patch_file gains a section arm replacing exactly the
  range read_file shows — empty new_string deletes the section, the
  heading rides new_string so renames work, expected_hash still guards.
- The splice is line-range math against the current text applied
  through updateFileContent, so a collaborator's concurrent edit
  outside the section survives (pinned by test). The sync-client
  getFileAst/updateFileAst path was rejected: writeQmd re-serializes
  the whole document (clobbering + normalization risk) for no gain.
- All section math is line-based, never byte offsets (automerge text
  is UTF-16); headings inside divs are out of scope for v1.

Build wiring: scripts/qmd-parser-pkg.mjs builds the pkg with a
git-state freshness stamp (no stale-parser trap, no-op when fresh);
bundle.mjs marks the package external and stages it into
dist-bundle/node_modules/wasm-qmd-parser (fail-closed, unit-tested);
vitest builds the pkg in globalSetup and points QUARTO_QMD_PARSER_SPEC
at it per-worker. Lazy singleton load — the 4.6 MB wasm never loads on
the plain-CRUD path (~4 ms init when it does).

Tests: 38 new (AST units against the real parser, get_outline,
read/patch section arms incl. the CRDT-merge case, staging units);
suite 462 passed + 3 skips. Tool budget: 20/24 data tools (23/24 with
auth). Instructions + docs/tools/q2-mcp.qmd updated; conformance,
hub-mcp, and inspector-smoke pins updated for the new surface.
… tier (CAP-11; bd-738mbhpj)

The parser pkg needs a wasm32 toolchain (wasm-pack + clang), which two
bundle-consuming environments deliberately lack:

- CI workspace-ts-suites job (documented as Rust/WASM-free): the vitest
  global setup now treats the build as best-effort — on failure it
  leaves a marker file and the 38 parser-tier tests skipIf, the same
  gated-tier pattern as the network-gated live tests. The tier runs in
  the toolchain-rich test-suite job instead (new wasm-pack step +
  explicit vitest invocation), keeping full CI coverage on both OS legs.
- Release pipeline: the wasm is platform-independent, so web-payloads
  builds it once and every bundle leg (per-target incl. Windows/musl,
  plus the standalone hub-mcp-bundle job, which gained
  needs: web-payloads + artifact download) stages it without
  rebuilding. The git-state freshness stamp now excludes the pkg dirs
  from its dirty scan so a downloaded artifact reads as fresh, not as
  untracked dirt (status is conversion-normalized, so CRLF checkouts
  stay clean). The hub-mcp-bundle verify step now fails closed unless
  the parser files are staged AND parse a document on the runner.

Runbook updated for the new bundle content. Full hub-mcp suite green
(462 passed + 3 skips); skip path verified end-to-end by simulating a
toolchain-less leg (pkg absent, no wasm-pack -> marker -> 38 skips).
…ender (CAP-12; bd-738mbhpj)

One opt-in tool (ERG-5 rule (a): the plan's render_project/render_file
pair consolidates to render with an optional path). Materializes the
project from the live sync state to a temp dir, runs
q2 render --json-errors, and returns structured diagnostics (Q- codes,
file/line/column relativized out of the temp dir) plus the produced
outputs; the temp dir is always removed.

- Three render modes, discovered against the real binary: q2 render .
  without _quarto.yml is Q-7-7 and bare q2 render is Q-7-3, so loose
  projects render as an explicit .qmd list (mode 'files', capped at
  500) while project dirs get the true directory render.
- Security: rendering executes project code, so the tool exists only
  under --allow-render and never with --read-only (the stricter gate
  wins); annotations honest (readOnlyHint false, openWorldHint true);
  timeout SIGTERM->SIGKILL, streams capped, path-escape entries refused
  at materialization, client cancel kills the child.
- The launcher injects QUARTO_Q2_PATH=current_exe (user env wins, same
  pattern as BP-10's version injection); standalone falls back to q2 on
  PATH with an actionable ENOENT error naming the variable.

Tests: 15 new — fake-q2 wire cases (both NDJSON envelope shapes,
timeout, outputs, spawn error), materialization + escape guard, the
three listing gates, and the plan-spec real-binary test (Q-17-2 with
file/line/column, gated on target/debug/q2). Launcher: 3 injection
units. Suite 477 passed + 3 skips. Instructions + docs page updated
(security model documented).
…; bd-738mbhpj)

The docs tool answers Quarto usage questions from the corpus embedded
in the q2 binary — the same pages q2 docs llms serves, with no project
connection and no network. One tool, two arms (ERG-5(a)): query ranks
pages (title hits outrank body; multi-word queries tokenize so
AND-matches float up; snippets pick the highest-term-density line),
page fetches one page's markdown by any accepted href spelling with
near-miss suggestions.

Seam: one subprocess per process lifetime — q2 docs llms --full via
QUARTO_Q2_PATH (the CAP-12 launcher injection doubles as the CAP-13
seam; PATH fallback for standalone). The corpus's per-page
---\ntitle:/url: markers make attribution exact, so everything after
the first fetch serves from the in-process cache. Chosen over
launcher-extracted doc dirs and bundle staging (no second code path,
no ~1 MB duplication).

bd-b6cocsxw ships with it: q2 docs llms --list --json emits
[{href,title}] and --embed-info --json emits
{placeholder,commit,dirty,pages}, gated by a clap jsonable group; unit
+ CLI integration tests in both embed states. docs/guides/embedded-docs
documents the flag.

Tests: 19 new on the TS side (corpus units, fake-q2 tool cases incl.
placeholder-embed error and read-only listing, conformance golden with
a stubbed corpus) + 4 Rust. Suite 496 passed + 3 skips. bd-dn81ol95 and
bd-b6cocsxw closed. Tool budget: 21/24 data, 24/24 with auth — at the
ceiling.
It deletes shared index state that drives other collaborators' preview
engines (the Q-3 invisible-shared-mutation class); the CAP-12 render
loop never touches hub-side captures; CAP-2's get_project_info surface
already lets the agent observe and advise. No tool.
…k (bd-738mbhpj)

Three new eval tasks: outline-and-section-edit (CAP-11 loop),
docs-lookup (CAP-13, requiresQ2), fix-render-error (CAP-12 loop,
requiresQ2). The runner opts its server into --allow-render, injects
QUARTO_Q2_PATH=target/debug/q2 when built, and skips q2-dependent tasks
when absent.

The first run exposed a real deployment gap: the tsc dist/ build (used
by the eval, dev runs, and the CI smoke check) has no staged parser —
only dist-bundle/ had one — so CAP-11 tools degraded to the error path
(the agent recovered via read_file + old_string, exactly the designed
fallback). qmd-parser.ts now resolves env-spec -> staged package ->
repo-relative pkg-nodejs fallback, so in-repo dist runs work with zero
staging; the bundle path is unchanged.

Clean full run: 15/15 PASS, median 5 turns (Phases 2/3: 5 — no
regression), zero isError except the DESIGNED stale-hash refusal in
stale-write-recovery. Transcripts: eval/results/2026-10-06T23-22-28/.
The outline rerun shows the intended sequence: get_outline -> read_file
section -> patch_file section (expected_hash) -> verify.
Three fixes the real-binary e2e surfaced:

1. read_file's section arm reported truncation against the FILE, so a
   complete section read claimed truncated: true with a continuation
   into the next section. Truncation is now reported against the
   section (complete => truncated: false, next_offset: null); a
   max_bytes-cut section points its continuation inside the section.
   windowLines exposes its end line; two regression tests added.
2. The loose-files render mode passed every .qmd in one invocation —
   but q2 refuses multiple paths without a project (Q-7-4). It now
   runs one q2 render per file under a shared deadline, unioning
   diagnostics/outputs and reporting the first nonzero exit. The
   fake-q2 stub is argv-aware (fails only for a named file); a
   two-file union test pins the batch behavior.
3. q2 docs llms --json alone was accepted: clap's group-requires does
   not propagate through the tuple-variant subcommand, so the pairing
   rule is enforced in DocsLlmsArgs::mode() instead. The
   cargo xtask verify run caught this after the local run masked it.

Also cargo fmt drift from heredoc-appended tests.

Phase 4 completion record added to the plan: e2e transcript summary
(22 tools with --allow-render; outline/section/docs/render all
exercised against the real binary with output inspected), eval table
(15/15 PASS, median 5 turns, zero unintended isError), and the phase
gate (verify 14/14, suite 498 passed + 3 skips). Tool budget noted as
at-ceiling (24/24 with auth).
…egs (bd-7tk3i1q3)

f0fd2b8 made workspace-ts-suites deliberately toolchain-free (marker +
skipIf for the 38 qmd-* parser-tier tests, tier re-run in the
toolchain-rich test-suite job), but two parser consumers escaped the
gate and failed both OS legs of run 37584171133:

- conformance.test.ts: the golden-result loop called get_outline
  unconditionally; without the parser the tool errors by design, so the
  assertion failed. The golden case now carries requiresParser and the
  execution loop skips such cases when PARSER_UNAVAILABLE (registration
  coverage is unaffected).
- bundle.test.ts: beforeAll runs bundle.mjs, which stages the
  wasm-qmd-parser pkg and hard-fails without a wasm32 toolchain. The
  suite is now describe.skipIf(PARSER_UNAVAILABLE) with hooks scoped
  inside the suite.

Both files join the test-suite job's parser-tier vitest invocation so
the gated coverage (including the bundle smoke test) still gates CI on
both OS legs. Verified both directions: toolchain-less simulation
(restricted PATH, pkg absent) reproduces the exact CI errors then goes
green with bundle.test.ts skipped; with the toolchain, the full suite
passes 55 files / 498 tests and the updated CI command passes 5 files
/ 75 tests.
@shikokuchuo
shikokuchuo force-pushed the braid/bd-738mbhpj-hub-mcp-phase-4-quarto-intelligence branch from 9ae3097 to 5517e68 Compare October 7, 2026 13:45

This branch has not been deployed

No deployments
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.

2 participants