Scaffold Data App
Scaffolds a new Tableau data app workspace: a starter Tableau viz (worksheet) extension that
queries a published datasource live via the Extensions API. Given a single datappName, the tool
derives the extension package id and display name and returns a ready-to-edit workspace — a
workbook plus an extension package containing index.html and a src/app.js starter you author
the query and visualization into, and a JSON manifest.json (see
Package manifest).
This tool only scaffolds and names the app — it does not wire a datasource, author query logic, build, publish, or embed data. Those remain separate steps.
This tool is gated behind the data-apps feature flag, which defaults to false in
features.json. It is unavailable unless an administrator enables data-apps. See
Feature Flags.
Required permissions
- Site Role: Requires Explorer (Can Publish) role or higher
- API scope:
tableau:packages:readis required. When API-scope enforcement is enabled, callers with older tokens missing this scope must reauthorize to request it. - Server feature: The experimental packages API requires the Tableau
Packagesfeature flag.
APIs called
GET /api/exp/sites/{siteId}/packages/allowed-origins — reads the site's external origin policy.
This request is required and runs before the workspace is prepared or uploaded. If it fails,
the tool returns an error instead of a scaffolded workspace.
Required arguments
datappName
Name for the new data app. Used verbatim as the workbook (.twb) filename and the extension
display name, and slugified into the extension package id (com.tableau.mcp.<slug>). Letters,
digits, spaces, dot, underscore, and hyphen only; must start and end with a letter or digit; no
path separators or ..; 1–100 characters.
Example: Sales Demo → package id com.tableau.mcp.sales-demo
Derived values
From datappName the tool derives the following identity:
-
package id:
com.tableau.mcp.<slug(datappName)>— lowercase, non-alphanumeric runs collapsed to a single hyphen. -
display name:
datappNameverbatim. -
version:
1.0.0. -
author:
Tableau MCP.
The version and author are fixed defaults, not derived per call. All four values are placeholders
in the template (TODO-MANIFEST-ID, TODO App Name, TODO-MANIFEST-VERSION, TODO Author Name),
filled by the postUnzip edits in data-app.trex and manifest.json (the version also in the
.twb), so they always agree.
Package manifest
The extension package ships a JSON manifest at Packages/<package id>/manifest.json. After the
postUnzip edits, its id, version, name, and author match data-app.trex:
{
"id": "com.tableau.mcp.sales-demo",
"version": "1.0.0",
"name": "Sales Demo",
"author": "Tableau MCP",
"requestedOrigins": ""
}
requestedOrigins is always scaffolded empty. To let the app call external origins, set it to a
space-separated list of them, the same form as a Content-Security-Policy source list (for example
"https://api.example.com https://cdn.example.com"). Each one should also appear in the result's
allowedOrigins, the site's external origin allow-list. The result's requestedOrigins field
carries the finalized manifest's path and these same instructions.
Response behavior
Every successful result includes allowedOrigins, an array of the site's configured external
origins. An empty array is a valid result; a failed lookup is an error, not an empty policy.
It also includes requestedOrigins: the path of the finalized manifest.json (relative to the
unzip directory, after postUnzip) and instructions for declaring the app's external origins in
it. See Package manifest.
The result is a single object. Both output modes return the same static, un-substituted template
zip plus a postUnzip plan describing the identity edits/renames to apply after unzipping — they
differ only in transport, never in when substitution happens (always client-side, after unzipping):
-
Otherwise (disk output): the result's
filePathpoints at the template zip already on disk on the server — skip the download and unzip directly. -
MCP_S3_BUCKETconfigured (S3 output): the server uploads its own build artifact — the same template zip disk output serves — to S3 automatically on every call, then presigns a short-lived GET URL for exactly those bytes. There is no manual publish step and nothing persists in the bucket waiting to be trusted; the object is always what the server just wrote. The result'ss3URLpoints at the same un-substituted template — download it first, then unzip.
In both cases, postUnzip describes the literal find/replace edits and path renames to apply
after unzipping to finalize the workspace. Datasource wiring is never performed by this tool in
either mode — it is the caller's responsibility, applied to the finalized workbook after this tool
returns.
Example result (disk output)
{
"datappName": "Sales Demo",
"filePath": "/app/build/templates/data-app-template.zip",
"allowedOrigins": [],
"requestedOrigins": {
"file": "Sales Demo/Packages/com.tableau.mcp.sales-demo/manifest.json",
"instructions": "`requestedOrigins` in this manifest is scaffolded empty. If the app fetches from external origins, set it to a space-separated list of them (e.g. \"https://api.example.com https://cdn.example.com\"). Each one should also be in `allowedOrigins`."
},
"postUnzip": {
"instructions": "Finalize the workspace after unzipping: first apply every `edits` entry (a literal find/replace on the file at `file`), then apply `renames` in order. Every path is relative to the unzip directory.",
"edits": [
{
"file": "Data App Name/Data App Name.twb",
"replacements": [
{ "find": "TODO-MANIFEST-ID", "replace": "com.tableau.mcp.sales-demo" },
{ "find": "TODO App Name", "replace": "Sales Demo" },
{ "find": "TODO Sheet Name", "replace": "Sales Demo" },
{ "find": "TODO-MANIFEST-VERSION", "replace": "1.0.0" }
]
},
{
"file": "Data App Name/Packages/TODO-MANIFEST-ID/extensions/data-app.trex",
"replacements": [
{ "find": "TODO-MANIFEST-ID", "replace": "com.tableau.mcp.sales-demo" },
{ "find": "TODO App Name", "replace": "Sales Demo" },
{ "find": "TODO-MANIFEST-VERSION", "replace": "1.0.0" },
{ "find": "TODO Author Name", "replace": "Tableau MCP" }
]
},
{
"file": "Data App Name/Packages/TODO-MANIFEST-ID/manifest.json",
"replacements": [
{ "find": "TODO-MANIFEST-ID", "replace": "com.tableau.mcp.sales-demo" },
{ "find": "TODO App Name", "replace": "Sales Demo" },
{ "find": "TODO-MANIFEST-VERSION", "replace": "1.0.0" },
{ "find": "TODO Author Name", "replace": "Tableau MCP" }
]
}
],
"renames": [
{ "from": "Data App Name/Packages/TODO-MANIFEST-ID", "to": "Data App Name/Packages/com.tableau.mcp.sales-demo" },
{ "from": "Data App Name/Data App Name.twb", "to": "Data App Name/Sales Demo.twb" },
{ "from": "Data App Name", "to": "Sales Demo" }
]
}
}
Example result (S3 output)
{
"datappName": "Sales Demo",
"s3URL": "https://example-bucket.s3.amazonaws.com/...presigned...",
"allowedOrigins": [],
"requestedOrigins": {
"file": "Sales Demo/Packages/com.tableau.mcp.sales-demo/manifest.json",
"instructions": "`requestedOrigins` in this manifest is scaffolded empty. If the app fetches from external origins, set it to a space-separated list of them (e.g. \"https://api.example.com https://cdn.example.com\"). Each one should also be in `allowedOrigins`."
},
"postUnzip": {
"instructions": "Finalize the workspace after unzipping: first apply every `edits` entry (a literal find/replace on the file at `file`), then apply `renames` in order. Every path is relative to the unzip directory.",
"edits": [
{
"file": "Data App Name/Data App Name.twb",
"replacements": [
{ "find": "TODO-MANIFEST-ID", "replace": "com.tableau.mcp.sales-demo" },
{ "find": "TODO App Name", "replace": "Sales Demo" },
{ "find": "TODO Sheet Name", "replace": "Sales Demo" },
{ "find": "TODO-MANIFEST-VERSION", "replace": "1.0.0" }
]
},
{
"file": "Data App Name/Packages/TODO-MANIFEST-ID/extensions/data-app.trex",
"replacements": [
{ "find": "TODO-MANIFEST-ID", "replace": "com.tableau.mcp.sales-demo" },
{ "find": "TODO App Name", "replace": "Sales Demo" },
{ "find": "TODO-MANIFEST-VERSION", "replace": "1.0.0" },
{ "find": "TODO Author Name", "replace": "Tableau MCP" }
]
},
{
"file": "Data App Name/Packages/TODO-MANIFEST-ID/manifest.json",
"replacements": [
{ "find": "TODO-MANIFEST-ID", "replace": "com.tableau.mcp.sales-demo" },
{ "find": "TODO App Name", "replace": "Sales Demo" },
{ "find": "TODO-MANIFEST-VERSION", "replace": "1.0.0" },
{ "find": "TODO Author Name", "replace": "Tableau MCP" }
]
}
],
"renames": [
{ "from": "Data App Name/Packages/TODO-MANIFEST-ID", "to": "Data App Name/Packages/com.tableau.mcp.sales-demo" },
{ "from": "Data App Name/Data App Name.twb", "to": "Data App Name/Sales Demo.twb" },
{ "from": "Data App Name", "to": "Sales Demo" }
]
}
}