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.
On the Desktop and combined servers the same tool name registers a different
implementation. Pass session, datappName, worksheetId (External API worksheet id), and
datasourceId (External API datasource id). Set
TABLEAU_DATA_APP_WORKSPACE_DIR
to an absolute workspace root first. A successful result includes packageId, sourceDir,
worksheetFile, extensionsDatasourceId (Extensions API id stamped into the starter
BOUND_DATASOURCE_ID), worksheetId, and worksheetName (live sheet name). Apply the fragment
with apply-worksheet, passing worksheetId as worksheetName. The rest of this page documents
the web contract.
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",
"manifestVersion": "0.1",
"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.
Allow network egress to the download host
In S3 output mode the agent downloads the template from s3URL, and it later uploads the workbook
the same way. If the agent runs in a sandbox with restricted network access, such as Claude's code
execution environment, allow the S3 host for your pod or the download fails. In Claude, go to
Settings → Capabilities → Allow network egress and add the host under Additional allowed
domains. See Popular Client Integrations for
client steps.
Tableau MCP hands files to and from your agent through short-lived presigned S3 URLs. These tools use them:
scaffold-data-appreturns ans3URLthe agent downloads the starter template from.request-workbook-uploadreturns anuploadUrlthe agent uploads the workbook to beforepublish-workbook.
The agent's own environment makes these requests, not the Tableau MCP server. If that environment restricts outbound network access (a sandbox, a code-execution container, or an organization allowlist), you must allow the S3 host for your Tableau pod. This is a client configuration step: nothing on the Tableau side can change it.
If the host is blocked, the download or upload fails with a network or 403 error. Allow the host
and retry. Don't work around the failure with a custom script against the Tableau REST API, because
that bypasses the checks and consent step in the MCP publish flow.
Find your pod in your site URL (for https://10ax.online.tableau.com, the pod is 10ax), then
allow its host.
Production pods
| Pod | S3 host to allow |
|---|---|
| UWC | uwc-tabmcp-content.s3.us-west-2.amazonaws.com |
| UW2B | uw2b-tabmcp-content.s3.us-west-2.amazonaws.com |
| CAA | caa-tabmcp-content.s3.ca-central-1.amazonaws.com |
| UEA | uea-tabmcp-content.s3.us-east-1.amazonaws.com |
| 10AY | 10ay-tabmcp-content.s3.us-west-2.amazonaws.com |
| UEB | ueb-tabmcp-content.s3.us-east-1.amazonaws.com |
| DUB01 | dub01-tabmcp-content.s3.eu-central-1.amazonaws.com |
| 10AZ | 10az-tabmcp-content.s3.us-west-2.amazonaws.com |
| UKA | uka-tabmcp-content.s3.eu-west-2.amazonaws.com |
| UE1 | ue1-tabmcp-content.s3.us-east-1.amazonaws.com |
| EW1A | ew1a-tabmcp-content.s3.eu-central-1.amazonaws.com |
| ANAA | anaa-tabmcp-content.s3.ap-northeast-1.amazonaws.com |
| ASAA | asaa-tabmcp-content.s3.ap-southeast-2.amazonaws.com |
| 10AX | 10ax-tabmcp-content.s3.us-west-2.amazonaws.com |
| SING | sing-tabmcp-content.s3.ap-southeast-1.amazonaws.com |
| UEC | uec-tabmcp-content.s3.us-east-1.amazonaws.com |
| INDO | indo-tabmcp-content.s3.ap-southeast-3.amazonaws.com |
| PDCHA | pdcha-tabmcp-content.s3.eu-central-2.amazonaws.com |
| PDINA | pdina-tabmcp-content.s3.ap-south-2.amazonaws.com |
| PDKRA | pdkra-tabmcp-content.s3.ap-northeast-2.amazonaws.com |
Using your own blob storage
If you run your own Tableau MCP server and set
MCP_S3_BUCKET, the pod hosts above don't
apply. Presigned URLs point at your bucket instead, so allow your bucket's host:
<your-bucket>.s3.<region>.amazonaws.com
Use the bucket name from MCP_S3_BUCKET and the region from AWS_DEFAULT_REGION. If you're not
sure of the exact host, call request-workbook-upload once and read the host from the returned
uploadUrl. To cover several buckets in one region, allow *.s3.<region>.amazonaws.com.
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" }
]
}
}