Skip to content

feat: prompt-driven authoring (3 tools + deterministic planner), official-ecosystem alignment, sidecar port resilience, and renderable embedded-extract dashboards - #4

Merged
SebAustin merged 16 commits into
mainfrom
feat/prompt-driven-authoring
Jul 21, 2026
Merged

feat: prompt-driven authoring (3 tools + deterministic planner), official-ecosystem alignment, sidecar port resilience, and renderable embedded-extract dashboards#4
SebAustin merged 16 commits into
mainfrom
feat/prompt-driven-authoring

Conversation

@SebAustin

Copy link
Copy Markdown
Owner

Summary

This branch (14 commits ahead of main) adds prompt-driven authoring to the Tableau publish MCP server: an agent can turn a file + a business question into a published datasource and a dashboard that actually renders on Tableau Cloud. It also aligns the project to the official Tableau ecosystem (a real TWB XSD validation gate, a TSC-matching chunk boundary, repositioned docs), hardens sidecar startup against a stale-port crash, and reworks the dashboard render path to embed the .hyper extract so workbooks are self-contained. The live render criterion (E2E-3) was proven at the end of the branch — both a datasource and a rendering dashboard workbook were published to Cloud. Gate is green: 87 TS + 114 Python = 201 tests.

What's in this PR

1. Prompt-driven authoring (3 new MCP tools, 11 → 14)

Additive — no existing tool is removed or renamed (src/index.ts registers all 14 tools).

  • create_datasource_from_file — multi-format ingest (csv/json/jsonl/xlsx/parquet) → .hyper/.tdsx → publish; returns {datasourceLuid, contentUrl, url} plus the real column schema read back from the .hyper (src/tools/createDatasourceFromFile.ts, sidecar/hyper_builder.py).
  • design_dashboard — a deterministic, stateless BI planner (src/planner/*: field inference → marks → audience clamp → plan) with 4 modes (autonomous / interview / interview_followup / directed) and 4 audiences; side-effect-free, returns a DashboardPlan or clarifying questions (src/tools/designDashboard.ts).
  • build_from_plan — turns a plan into a real <dashboard>/<zones> workbook and publishes it; returns {workbookLuid, url, datasourceLuid?} (src/tools/buildFromPlan.ts).
  • Two new FastAPI sidecar routes: POST /datasource/from-file and POST /workbook/dashboard.

2. Official-ecosystem alignment (incl. the TWB XSD gate)

  • Official TWB XSD fidelity gate. The genuine 7,586-line twb_2026.1.0.xsd (vendored from tableau/tableau-document-schemas at a pinned SHA, with W3C xml.xsd and PROVENANCE.md) now validates the sidecar's actual generated output (build_twb_xml, build_embedded_twb_xml). The gate was proven non-no-op: injecting a bogus element makes validate() return False. Parser is XXE/billion-laughs hardened (resolve_entities=False, no_network=True, load_dtd=False, huge_tree=False) with a behavioral regression-guard test. lxml is dev-only — never imported at runtime (sidecar/tests/test_twb_schema_validation.py, sidecar/tests/schemas/).
  • Schema-valid TWB output. Fixed real structural defects so generated workbooks pass the XSD (<simple-id>, <style>, <aggregation>, populated <cards>/<viewpoints>, dashboards-before-windows ordering).
  • Chunk boundary aligned to official TSC. selectPublishStrategy switched > limit>= limit, so an exactly-64-MiB file now takes the chunked path (src/restClient.ts; boundary oracle in tests/restClient.test.ts). Chunk size was already 64 MiB on main — unchanged.
  • Repositioning + 14-tool docs across README.md, docs/relationship_to_official.md, docs/tool_reference.md, docs/architecture.md, and a new docs/adr/0005-deterministic-stateless-planner.md.

3. Sidecar port resilience (f623355)

sidecarPort is now optional; when SIDECAR_PORT is unset the TS layer auto-selects a free ephemeral port (with a bounded retry on the bind race) instead of always binding 8899. This fixes a startup crash where a stale sidecar holding 8899 bricked the run ([Errno 48] address already in use). Proven live: the sidecar started while 8899 was still occupied. The token still binds 127.0.0.1 only and is passed via env, not argv (src/config.ts, src/sidecar.ts).

4. Dashboard render via embedded extract (the render keystone)

Dashboards published by build_from_plan originally returned Tableau 400011 "Dashboard references sheet 'Sheet 1' which has no visual representation." Diagnosed (against 7 real reference workbooks + an isolating worksheet-only publish) as a layered cause; each fix verified live:

  1. Embed the extract. The workbook now embeds the .hyper via a federated connection (a self-contained .twbx) instead of referencing the published datasource via sqlproxy, which Tableau Cloud would not render. The governed published datasource is still created separately.
  2. Per-sheet <viewpoints> (the root cause). The dashboard window emitted an empty <viewpoints/>. A "viewpoint" is Tableau's term for a sheet's visual on a dashboard, so an empty list literally means no visual representation. Now one <viewpoint name="…"><zoom type="entire-view"/></viewpoint> is emitted per sheet in both builders.
  3. layout-flow zones. Worksheet zones nest in a type-v2="layout-flow" container (each with <layout-cache>), with a <style> child on <dashboard>.
  4. Real-column binding + populated <cards> so worksheets bind to the datasource's actual columns.

The shipped tool (build_from_plan), not just the demo, threads the .hyper into the embedded build; the no-embeddable-extract paths (pre-published datasourceLuid only, or a SQL query) fail loudly with an actionable error.

Breaking / behavior changes

  • build_from_plan requires datasourceSpec.filePath. A plan with only datasourceLuid, or a sql/query spec, is rejected at runtime — the embedded-extract render path needs a local file to embed. New-tool behavior (no prior consumer), but note a schema/runtime gap: DatasourceSpecSchema permits a sql-only spec and datasourceSpec is .optional(), so a schema-valid plan can still be rejected by the runtime guard. Also: design_dashboard does not emit datasourceSpec, so its plan must be augmented with datasourceSpec.filePath before calling build_from_plan (documented in README.md).
  • Sidecar default port now auto-selected. Anyone relying on the sidecar always being on 8899 (health probes, firewall rules) must now pin SIDECAR_PORT=8899 explicitly. Documented in .env.example.
  • engines.node bumped >=20>=22.7.5 to match the official Tableau server's Node floor. Installs on Node 20/21 will warn/fail under engine-strict. No Node-22.7.5-only API is actually required — the floor is for ecosystem alignment.
  • Chunk strategy boundary >>= (exactly 64 MiB → chunked). Only the exactly-64-MiB case changes; <64 MiB stays single, >64 MiB stays chunked.
  • Dashboard workbooks now embed the data (.twbx with the full .hyper, bounded by the 500 MB / 1M-row ingest caps) rather than referencing a published datasource. Expected for standalone rendering; a data-residency consideration worth noting.
  • url value semantics (shared publish path). publish() now returns the server-provided webpageUrl as url, falling back to the constructed LUID URL only when absent (previously url was always the constructed #/site/... form). Output shape is unchanged for publish_datasource/publish_workbook; only the url content may differ for consumers that pattern-matched the old format. publishWorkbook also now sends skipConnectionCheck=true, and getDatasource() now throws when no contentUrl resolves (was best-effort). These are internal/library-level behavior changes.

Testing

  • Full gate green: 87 TS + 114 Python = 201 tests. No focused/skipped tests; ESLint exit 0 and ruff --select F clean; npm audit --omit=dev → 0 vulnerabilities.
  • Official-XSD gate validates real production output and empirically rejects malformed XML (mutant probe confirmed).
  • Planner is a real priority-ordered engine exercised by behavioral tests (tests/planner.test.ts); build_from_plan E2E threads hyperPath into the dashboard build and asserts the LUID-only and SQL branches fail loud without building.
  • Embedded structure asserted at unit level (federated present, sqlproxy absent, Data/*.hyper in zip, [federated.*] refs, metadata-records, schema validity) and end-to-end via a real HTTP POST /workbook/dashboard round-trip (sidecar/tests/test_twb_embedded.py, test_server_new_routes.py).
  • Secret handling covered: PAT and sidecar token never logged or passed via argv; tests/secrets.test.ts green; mcp-smoke.ts asserts the PAT never appears in tool output.
  • E2E-3 proven live (2026-06-25): npm run demo:dashboard published a datasource (…/datasources/25936416) and a rendering dashboard workbook (…/workbooks/2420435) to site sebaustin.

Non-blocking follow-ups

Doc-accuracy drift (no confirmed code blocker; gate green and E2E-3 proven):

  • HIGH — DEPLOYMENT.md registered-tools table lists 6 tool names that do not exist (list_datasources, list_workbooks, set_datasource_permissions, set_workbook_permissions) and omits the 4 real ones (list_content, refresh_datasource, delete_content, set_permissions). Fix the table before relying on it. (spun off as a follow-up task)
  • HIGH — CODEBASE.md still describes the pre-feature 11-tool baseline (file map omits src/planner/, the 3 new tools, and the new routes; "Tech Debt" lists already-shipped items feat(interop): chain off the official server's metadata to auto-suggest workbook sheets #3/feat: prompt-driven authoring (3 tools + deterministic planner), official-ecosystem alignment, sidecar port resilience, and renderable embedded-extract dashboards #4 as open).
  • HIGH — ACCEPTANCE.md E2E-1 row documents a "no-datasource-spec → 0× publishDatasource" criterion that the shipped code and current test contradict (code now requires datasourceSpec.filePath); same stale criterion in REQUIREMENTS.md / feature PLAN.md. (spun off as a follow-up task)
  • MEDIUM — ACCEPTANCE.md E2E-3 criteria table still shows E2E-3 "⏳ gated / 16/17" while the appended bottom section declares it CLOSED (18/18, 201 tests). Reconcile the table.
  • MEDIUM — ACCEPTANCE.md DB-1 documents an obsolete <zone type="worksheet"> structure; shipped zones are identified by name alone with no type (the deliberate 400011 fix; architecture.md is correct).
  • LOW — Node-floor / test-count / FastAPI-version drift in DEPLOYMENT.md ("Node 22+", "75 TS + 71 Python", FastAPI 0.115.6) and CODEBASE.md (Node ≥20, 28/18 tests) vs the real ≥22.7.5 / 201 tests / FastAPI 0.121.0.
  • LOW — twb_builder docstrings claim default output is "byte-identical to pre-feature" (it isn't — the starter worksheet/window structure was re-baselined this branch); module docstring says <active id="1"/> but code/tests use id="-1".
  • LOW — TS↔server dashboards payload mismatch: the TS client sends a dashboards[].sheetTitles field the server's pydantic model drops (server always derives its own). Dead input today; harmless because every sheet title is passed anyway.
  • LOW — viewpoint test coverage gap: the per-sheet <viewpoints> population that fixes 400011 is the render keystone, but the only guarding test asserts <viewpoints> is present (not populated per sheet) and the embedded path has zero viewpoint assertions — an empty-<viewpoints/> regression would pass the whole 201-test gate. Production code populates them correctly today, but nothing locks it in. Worth a "one <viewpoint> per sheet name" assertion on the embedded builder.
  • INFO — repo/security hygiene clean: reference .twb/.twbx gitignored and untracked; npm pack ships only dist + 4 sidecar .py + lockfiles (no secrets/extracts/tests/XSDs); arbitrary local-file read via filePath is by-design and loopback+token-guarded; no debug/scratch leftovers.

🤖 Generated with Claude Code

Sebastien Henry and others added 16 commits June 24, 2026 15:41
- ignore gitignored .cursor/ in eslint so local `make ci` matches green CI
- add CODEBASE.md (onboarding map) and feature requirements
- fold in pre-existing in-flight hardening (restClient, twb_builder, verify scripts)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Plan loop converged in 2 iterations. Final surface: 3 new tools
(create_datasource_from_file, design_dashboard, build_from_plan) +
2 sidecar routes. BI_DESIGN.md is the normative chart/audience/layout spec.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the prompt-driven authoring layer the user asked for:

- create_datasource_from_file: build+publish a governed datasource from a
  file (csv/json/jsonl/xlsx/parquet) or the existing SQL-query path
- design_dashboard: deterministic, stateless BI planner — autonomous,
  interview (senior-BI-analyst clarifying questions), interview_followup,
  and directed modes; audience-aware (exec/analyst/operational/mixed)
- build_from_plan: the only side-effecting tool — consumes a DashboardPlan,
  optionally creates the datasource, builds a real dashboard (.twb with
  <dashboard>/<zones>) and publishes to Cloud

Sidecar gains /datasource/from-file and /workbook/dashboard routes; twb_builder
emits dashboard zones via a pure _tile_zones geometry (Σ==100000, no overlap),
with per-audience canvas sizes. Planner (src/planner/*) implements the normative
BI_DESIGN rules: field-role inference + identifier suppression, chart->mark
selection with documented gap fallbacks, and the 6-step audience clamp.

Guardrails: explicit project required, overwrite=false default, placeholder-token
and audience-invariant rejection before any publish, file-size cap + row clamp,
PAT/token never logged. Tools 11 -> 14. Gate green: 75 TS + 71 Python tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tecture, ADR, deployment)

- README: Tools table 11->14; prompt-driven authoring section (file-or-query
  datasource, three dashboard modes, audience), end-to-end example
- docs/tool_reference.md: full entries for the 3 new tools (params/returns/guardrails)
- docs/architecture.md: planner pipeline, new sidecar routes, dashboard XML, stateless boundary
- docs/adr/0005: deterministic stateless planner (LLM in the agent) decision record
- DEPLOYMENT.md: 14-tool capability list, new deps + install commands, gated demo command

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…adless criteria pass, E2E-3 gated)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reviewed Tableau's official GitHub org and adapted the project where the
evidence warranted (full review: docs/ecosystem-review/REVIEW.md).

Code:
- A1/A2: wire the official TWB XSD (tableau/tableau-document-schemas, pinned
  SHA) into the sidecar as a fidelity gate; the RED revealed 6 real structural
  defects our own tests never caught — fix them so build_twb_xml() output is
  schema-valid (dashboards-before-windows, <simple-id>, <style>, <aggregation>,
  <window> cards/viewpoints, <explain-data>). lxml configured XXE-safe.
- A3: align chunk boundary to official server-client-python: '>' -> '>=' so a
  file of exactly 64 MiB takes the chunked path (a single multipart request at
  exactly 64 MiB exceeds the cap once boundary overhead is added).

Docs:
- A4/A5: reframe positioning — the official MCP server (v2.18) now has
  Desktop-local apply-workbook authoring + admin-gated deletes; we remain the
  headless Cloud-publish authoring path. Refresh to the 14-tool surface, fix
  transport/auth/Node-floor claims, document TSC divergences and the deliberate
  non-adoptions (document-api-python = modify-only; tableau-ui = web-only).

Non-adoptions recorded so they are not re-litigated. Gate green: 75 TS + 83
Python = 158 tests; ruff + mypy --strict clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… startup

The TS layer spawned the Python sidecar on a hardcoded-default port 8899; a
stale sidecar (or anything) holding 8899 made startup hard-fail with
'address already in use'. Now the default path asks the OS for a free
ephemeral port (node:net bind on port 0) and retries on a fresh port up to 3
times to close the probe-then-bind race. An explicit SIDECAR_PORT is honored
and fails loudly on conflict (no silent retry). Loopback bind + per-spawn
X-Sidecar-Token unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…worksheet cards

Two fidelity fixes surfaced by the gated live dashboard demo:
- create_datasource_from_file now returns the real column schema (name +
  coarse dataType from the DataFrame dtypes); the demo feeds those to the
  planner as fieldHints so worksheets bind only to columns that exist in the
  published datasource (previously the demo guessed field names that weren't
  in the data).
- twb_builder emits a populated <cards> structure (columns/rows/marks strips)
  in each worksheet window instead of an empty <cards/>, matching real
  Tableau workbooks.

Both keep the official TWB XSD gate green. Datasource authoring + publish is
proven live. NOTE: the dashboard *workbook* publish still returns Tableau
400011 'sheet has no visual representation' — a dashboard-render semantic the
hand-built .twb does not yet satisfy; E2E-3 (live dashboard render) remains
open. Gate: 85 TS + 95 Python tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ender still open

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffed our output against 7 real rendering .twb files. Applied the
render-fidelity corrections each one exhibits:
- worksheet zones identified by @name with NO type attribute (was
  type="worksheet"); only the container carries @type-v2="layout-basic"
- worksheet windows emit <viewpoint/> between <cards> and <simple-id>
- dashboard window <active id="-1"> (was id="1", which pointed at the
  container zone)

All stay XSD-valid (official gate green). These are genuine corrections, but
the live dashboard publish still returns 400011 'no visual representation' —
root cause now isolated to the published-datasource (sqlproxy) field binding,
which none of the 7 (extract-embedded) references cover. Gate: 85 TS + 96 Python.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…iewpoints

The dashboard workbook now PUBLISHES AND RENDERS on Tableau Cloud (E2E-3).
Root cause of the long-standing 400011 'sheet has no visual representation':
the dashboard window emitted an empty <viewpoints/> — 'viewpoint' is Tableau's
term for a sheet's visual on a dashboard, so an empty list literally means the
referenced sheet has no visual. Fix chain (each necessary, verified live):

- embed the .hyper extract via a federated connection (self-contained .twbx),
  threading the hyper path from the datasource build into the workbook build
  (sqlproxy published-DS reference does not render; embedded does) — matches
  all 7 reference workbooks
- dashboard <zones>: worksheet zones nested inside a <zone type-v2=layout-flow>
  container (not directly in layout-basic), each with a <layout-cache>, plus a
  <style> child — per the real reference dashboards
- dashboard window <viewpoints> now lists a <viewpoint name=...> per sheet
  (in BOTH build_twb_xml and build_embedded_twb_xml)
- worksheets bind to the datasource's real columns; worksheet windows carry
  populated <cards>

Proven live: datasource + dashboard workbook both publish to Cloud. All stays
XSD-valid. Gate: 85 TS + 114 Python tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…pped tool

The dashboard render fix was wired into the demo script but not build_from_plan
itself, so an agent calling the tool still got a non-rendering sqlproxy workbook.
Now the tool threads hyperPath from buildDatasourceFromFile into
buildDashboardWorkbook (datasourceSpec.filePath branch), producing the same
embedded .twbx proven to render live. The LUID-only and SQL branches — which have
no embeddable extract — now fail loudly with an actionable error instead of
silently emitting a broken workbook. Tests assert the hyperPath threading and the
loud-fail branches so the regression is caught headlessly. Planner now derives
descriptive sheet titles (e.g. 'Revenue by Region') instead of 'Sheet 1'.

Verified SOLID (99.5/100). Gate: 87 TS + 114 Python tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ract + viewpoints)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reflect the shipped design across README, architecture, tool reference, and the
official-ecosystem doc: the dashboard workbook embeds the .hyper via a federated
connection (self-contained .twbx that renders on Cloud), while the governed
datasource is still published separately as a .tdsx. build_from_plan now requires
datasourceSpec.filePath to embed the extract; LUID-only / SQL-query calls fail
loudly. Reconciles 4 stale doc claims against the code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…test

Pre-merge review (GO) flagged non-blocking doc drift + one test gap:
- DEPLOYMENT.md/CODEBASE.md: correct tool list (real 14), Node >=22.7.5,
  test counts (87 TS / 114+1 Python), FastAPI 0.121.0, add src/planner + routes
- ACCEPTANCE.md: reconcile E2E-1 (build_from_plan requires datasourceSpec.filePath),
  E2E-3 (closed, 18/18, 201 tests), DB-1 (zones identified by name, no type)
- twb_builder docstrings: fix 'byte-identical to pre-feature' wording + active id=-1
- test_twb_embedded: assert one <viewpoint name> per sheet (locks the 400011 render fix)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
build_from_plan requires datasourceSpec.filePath and always publishes the
datasource (the embedded-extract render path), so E2E-1 is now '1x publishDatasource'
not '0x'. Fixes the stale '0x publishDatasource / no datasource spec' wording in
REQUIREMENTS.md and the feature PLAN.md (ACCEPTANCE.md E2E-1 was already corrected).
Also corrects the gate count to the verified 87 TS + 115 Python = 202 (the
per-sheet-viewpoints regression test bumped Python 114 -> 115).

Docs only; code and tests are correct and unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@SebAustin
SebAustin merged commit 964aa3b into main Jul 21, 2026
5 checks passed
SebAustin pushed a commit that referenced this pull request Jul 21, 2026
…behavior (carried forward from PR #4 pre-main-promotion)
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