feat: prompt-driven authoring (3 tools + deterministic planner), official-ecosystem alignment, sidecar port resilience, and renderable embedded-extract dashboards - #4
Merged
Conversation
- 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>
Closed
7 tasks
SebAustin
pushed a commit
that referenced
this pull request
Jul 21, 2026
…behavior (carried forward from PR #4 pre-main-promotion)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.hyperextract 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.tsregisters 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 aDashboardPlanor 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).POST /datasource/from-fileandPOST /workbook/dashboard.2. Official-ecosystem alignment (incl. the TWB XSD gate)
twb_2026.1.0.xsd(vendored fromtableau/tableau-document-schemasat a pinned SHA, with W3Cxml.xsdandPROVENANCE.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 makesvalidate()returnFalse. Parser is XXE/billion-laughs hardened (resolve_entities=False, no_network=True, load_dtd=False, huge_tree=False) with a behavioral regression-guard test.lxmlis dev-only — never imported at runtime (sidecar/tests/test_twb_schema_validation.py,sidecar/tests/schemas/).<simple-id>,<style>,<aggregation>, populated<cards>/<viewpoints>, dashboards-before-windows ordering).selectPublishStrategyswitched> limit→>= limit, so an exactly-64-MiB file now takes the chunked path (src/restClient.ts; boundary oracle intests/restClient.test.ts). Chunk size was already 64 MiB on main — unchanged.README.md,docs/relationship_to_official.md,docs/tool_reference.md,docs/architecture.md, and a newdocs/adr/0005-deterministic-stateless-planner.md.3. Sidecar port resilience (
f623355)sidecarPortis now optional; whenSIDECAR_PORTis 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 binds127.0.0.1only 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_planoriginally 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:.hypervia afederatedconnection (a self-contained.twbx) instead of referencing the published datasource viasqlproxy, which Tableau Cloud would not render. The governed published datasource is still created separately.<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.layout-flowzones. Worksheet zones nest in atype-v2="layout-flow"container (each with<layout-cache>), with a<style>child on<dashboard>.<cards>so worksheets bind to the datasource's actual columns.The shipped tool (
build_from_plan), not just the demo, threads the.hyperinto the embedded build; the no-embeddable-extract paths (pre-publisheddatasourceLuidonly, or a SQL query) fail loudly with an actionable error.Breaking / behavior changes
build_from_planrequiresdatasourceSpec.filePath. A plan with onlydatasourceLuid, or asql/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:DatasourceSpecSchemapermits a sql-only spec anddatasourceSpecis.optional(), so a schema-valid plan can still be rejected by the runtime guard. Also:design_dashboarddoes not emitdatasourceSpec, so its plan must be augmented withdatasourceSpec.filePathbefore callingbuild_from_plan(documented inREADME.md).SIDECAR_PORT=8899explicitly. Documented in.env.example.engines.nodebumped>=20→>=22.7.5to 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.>→>=(exactly 64 MiB → chunked). Only the exactly-64-MiB case changes;<64 MiBstays single,>64 MiBstays chunked..twbxwith 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.urlvalue semantics (shared publish path).publish()now returns the server-providedwebpageUrlasurl, falling back to the constructed LUID URL only when absent (previouslyurlwas always the constructed#/site/...form). Output shape is unchanged forpublish_datasource/publish_workbook; only theurlcontent may differ for consumers that pattern-matched the old format.publishWorkbookalso now sendsskipConnectionCheck=true, andgetDatasource()now throws when nocontentUrlresolves (was best-effort). These are internal/library-level behavior changes.Testing
ruff --select Fclean;npm audit --omit=dev→ 0 vulnerabilities.tests/planner.test.ts);build_from_planE2E threadshyperPathinto the dashboard build and asserts the LUID-only and SQL branches fail loud without building.sqlproxyabsent,Data/*.hyperin zip,[federated.*]refs, metadata-records, schema validity) and end-to-end via a real HTTPPOST /workbook/dashboardround-trip (sidecar/tests/test_twb_embedded.py,test_server_new_routes.py).tests/secrets.test.tsgreen;mcp-smoke.tsasserts the PAT never appears in tool output.npm run demo:dashboardpublished a datasource (…/datasources/25936416) and a rendering dashboard workbook (…/workbooks/2420435) to sitesebaustin.Non-blocking follow-ups
Doc-accuracy drift (no confirmed code blocker; gate green and E2E-3 proven):
DEPLOYMENT.mdregistered-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)CODEBASE.mdstill describes the pre-feature 11-tool baseline (file map omitssrc/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).ACCEPTANCE.mdE2E-1 row documents a "no-datasource-spec → 0× publishDatasource" criterion that the shipped code and current test contradict (code now requiresdatasourceSpec.filePath); same stale criterion inREQUIREMENTS.md/ featurePLAN.md. (spun off as a follow-up task)ACCEPTANCE.mdE2E-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.ACCEPTANCE.mdDB-1 documents an obsolete<zone type="worksheet">structure; shipped zones are identified bynamealone with notype(the deliberate 400011 fix;architecture.mdis correct).DEPLOYMENT.md("Node 22+", "75 TS + 71 Python", FastAPI 0.115.6) andCODEBASE.md(Node ≥20, 28/18 tests) vs the real ≥22.7.5 / 201 tests / FastAPI 0.121.0.twb_builderdocstrings 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 useid="-1".dashboardspayload mismatch: the TS client sends adashboards[].sheetTitlesfield the server's pydantic model drops (server always derives its own). Dead input today; harmless because every sheet title is passed anyway.<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..twb/.twbxgitignored and untracked;npm packships onlydist+ 4 sidecar.py+ lockfiles (no secrets/extracts/tests/XSDs); arbitrary local-file read viafilePathis by-design and loopback+token-guarded; no debug/scratch leftovers.🤖 Generated with Claude Code