Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,38 @@ make it easier for developers to build AI applications that integrate with Table
## Official Documentation

https://tableau.github.io/tableau-mcp/
## New Tool: Generate Workbook XML

The `generate-workbook-xml` tool creates a Tableau TWB (XML) string that connects to a published data source on Data Server.

Parameters:

- `datasourceName` (required): The published data source display name (friendly name).
- `publishedDatasourceId` (required): The published datasource's repository ID.
- `datasourceCaption` (optional): Caption in the workbook; defaults to `datasourceName`.
- `revision` (optional): Revision string; defaults to `1.0`.
- `worksheetName` (optional): The initial sheet name; defaults to `Sheet 1`.

Output is a TWB XML string you can save to a `.twb` file. Server URL and site are taken from the MCP server configuration (`SERVER`, `SITE_NAME`).

## New Tool: Inject Viz Into Workbook XML

The `inject-viz-into-workbook-xml` tool accepts an existing TWB XML string and injects a basic visualization into a worksheet by:
- Referencing the datasource in the sheet's `<view>` block
- Adding `<datasource-dependencies>` for the specified fields
- Binding fields to the `<rows>` and `<cols>` shelves

Parameters:

- `workbookXml` (required): The TWB XML string to modify.
- `worksheetName` (optional): Target sheet; default is the first.
- `datasourceConnectionName` (optional): Datasource `name` to reference; default is the first found.
- `datasourceCaption` (optional): Datasource caption used in `<view>`.
- `columns` (required): Array of dimensions for the Columns shelf.
- `rows` (required): Array of `{ field, aggregation? }` measures for the Rows shelf.

Returns an updated TWB XML string you can save to `.twb`.


## Quick Start

Expand Down
4 changes: 3 additions & 1 deletion src/tools/toolName.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
'query-datasource',
'get-datasource-metadata',
'get-workbook',
'generate-workbook-xml',
'inject-viz-into-workbook-xml',
'get-view-data',
'get-view-image',
'list-all-pulse-metric-definitions',
Expand All @@ -28,7 +30,7 @@

export const toolGroups = {
datasource: ['list-datasources', 'get-datasource-metadata', 'query-datasource'],
workbook: ['list-workbooks', 'get-workbook'],
workbook: ['list-workbooks', 'get-workbook', 'generate-workbook-xml', 'inject-viz-into-workbook-xml'],

Check failure on line 33 in src/tools/toolName.ts

View workflow job for this annotation

GitHub Actions / build (>=22.7.5 <23)

Replace `'list-workbooks',·'get-workbook',·'generate-workbook-xml',·'inject-viz-into-workbook-xml'` with `⏎····'list-workbooks',⏎····'get-workbook',⏎····'generate-workbook-xml',⏎····'inject-viz-into-workbook-xml',⏎··`
view: ['list-views', 'get-view-data', 'get-view-image'],
pulse: [
'list-all-pulse-metric-definitions',
Expand Down
4 changes: 4 additions & 0 deletions src/tools/tools.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getSearchContentTool } from './contentExploration/searchContent.js';

Check failure on line 1 in src/tools/tools.ts

View workflow job for this annotation

GitHub Actions / build (>=22.7.5 <23)

Run autofix to sort these imports!
import { getGetDatasourceMetadataTool } from './getDatasourceMetadata/getDatasourceMetadata.js';
import { getListDatasourcesTool } from './listDatasources/listDatasources.js';
import { getGeneratePulseMetricValueInsightBundleTool } from './pulse/generateMetricValueInsightBundle/generatePulseMetricValueInsightBundleTool.js';
Expand All @@ -12,6 +12,8 @@
import { getGetViewImageTool } from './views/getViewImage.js';
import { getListViewsTool } from './views/listViews.js';
import { getGetWorkbookTool } from './workbooks/getWorkbook.js';
import { getGenerateWorkbookXmlTool } from './workbooks/generateWorkbookXml.js';
import { getInjectVizIntoWorkbookXmlTool } from './workbooks/injectVizIntoWorkbookXml.js';
import { getListWorkbooksTool } from './workbooks/listWorkbooks.js';

export const toolFactories = [
Expand All @@ -25,6 +27,8 @@
getListPulseMetricSubscriptionsTool,
getGeneratePulseMetricValueInsightBundleTool,
getGetWorkbookTool,
getGenerateWorkbookXmlTool,
getInjectVizIntoWorkbookXmlTool,
getGetViewDataTool,
getGetViewImageTool,
getListWorkbooksTool,
Expand Down
198 changes: 198 additions & 0 deletions src/tools/workbooks/generateWorkbookXml.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';

Check failure on line 1 in src/tools/workbooks/generateWorkbookXml.ts

View workflow job for this annotation

GitHub Actions / build (>=22.7.5 <23)

Run autofix to sort these imports!
import { Ok } from 'ts-results-es';
import { randomUUID } from 'node:crypto';
import { z } from 'zod';

import { getConfig } from '../../config.js';
import { Server } from '../../server.js';
import { Tool } from '../tool.js';

const paramsSchema = {
datasourceName: z.string().trim().nonempty(),
publishedDatasourceId: z.string().trim().nonempty(),
// Optional overrides; sensible defaults are derived from config and datasourceName
datasourceCaption: z.string().trim().nonempty().optional(),
revision: z.string().trim().nonempty().default('1.0').optional(),
worksheetName: z.string().trim().nonempty().default('Sheet 1').optional(),
} as const;

function sanitizeForId(input: string): string {
return input.replace(/[^A-Za-z0-9_-]/g, '');
}

function escapeXmlAttribute(value: string): string {
return value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
}

function generateSqlProxyConnectionName(): string {
// Tableau-generated names look like: sqlproxy.<random>
const random = Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
return `sqlproxy.${random.slice(0, 28)}`;
}

function buildWorkbookXml({
siteName,
hostname,
port,
channel,
datasourceName,
datasourceCaption,
publishedDatasourceId,
revision,
worksheetName,
}: {
siteName: string;
hostname: string;
port: string;
channel: 'http' | 'https';
datasourceName: string;
datasourceCaption: string;
publishedDatasourceId: string;
revision: string;
worksheetName: string;
}): string {
const connectionName = generateSqlProxyConnectionName();
const uuid = randomUUID().toUpperCase();

const pathDatasources = siteName
? `/t/${escapeXmlAttribute(siteName)}/datasources`
: `/datasources`;
const siteAttr = siteName ? ` site='${escapeXmlAttribute(siteName)}'` : '';

return `<?xml version='1.0' encoding='utf-8' ?>

<!-- build main.0.0000.0000 -->
<workbook original-version='18.1' source-build='0.0.0 (0000.0.0.0)' source-platform='win' version='18.1' xmlns:user='http://www.tableausoftware.com/xml/user'>
<document-format-change-manifest>
<AnimationOnByDefault />
<ISO8601DefaultCalendarPref />
<MarkAnimation />
<ObjectModelEncapsulateLegacy />
<ObjectModelTableType />
<SchemaViewerObjectModel />
<SheetIdentifierTracking />
<WindowsPersistSimpleIdentifiers />
</document-format-change-manifest>
<datasources>
<datasource caption='${escapeXmlAttribute(datasourceCaption)}' inline='true' name='${escapeXmlAttribute(connectionName)}' version='18.1'>
<repository-location id='${escapeXmlAttribute(publishedDatasourceId)}' path='${pathDatasources}' revision='${escapeXmlAttribute(revision)}'${siteAttr} />
<connection channel='${channel}' class='sqlproxy' dbname='${escapeXmlAttribute(datasourceName)}' local-dataserver='' port='${escapeXmlAttribute(port)}' server='${escapeXmlAttribute(hostname)}' username=''>
<relation name='sqlproxy' table='[sqlproxy]' type='table' />
</connection>
<aliases enabled='yes' />
</datasource>
</datasources>
<worksheets>
<worksheet name='${escapeXmlAttribute(worksheetName)}'>
<table>
<view>
<datasources />
<aggregation value='true' />
</view>
<style />
<panes>
<pane selection-relaxation-option='selection-relaxation-allow'>
<view>
<breakdown value='auto' />
</view>
<mark class='Automatic' />
</pane>
</panes>
<rows />
<cols />
</table>
<simple-id uuid='{${uuid}}' />
</worksheet>
</worksheets>
<windows source-height='30'>
<window class='worksheet' maximized='true' name='${escapeXmlAttribute(worksheetName)}'>
<cards>
<edge name='left'>
<strip size='160'>
<card type='pages' />
<card type='filters' />
<card type='marks' />
</strip>
</edge>
<edge name='top'>
<strip size='2147483647'>
<card type='columns' />
</strip>
<strip size='2147483647'>
<card type='rows' />
</strip>
<strip size='31'>
<card type='title' />
</strip>
</edge>
</cards>
<simple-id uuid='{${randomUUID().toUpperCase()}}' />
</window>
</windows>
</workbook>`;
}

export const getGenerateWorkbookXmlTool = (server: Server): Tool<typeof paramsSchema> => {
const generateWorkbookXmlTool = new Tool({
server,
name: 'generate-workbook-xml',
description:

Check failure on line 144 in src/tools/workbooks/generateWorkbookXml.ts

View workflow job for this annotation

GitHub Actions / build (>=22.7.5 <23)

Delete `⏎·····`
`Generates a Tableau TWB (workbook) XML string that connects to a specified published datasource (Data Server). Use the output to save a .twb file.`,
paramsSchema,
annotations: {
title: 'Generate Workbook XML',
readOnlyHint: false,
openWorldHint: false,
},
callback: async (
{ datasourceName, publishedDatasourceId, datasourceCaption, revision, worksheetName },
{ requestId },
): Promise<CallToolResult> => {
const config = getConfig();
return await generateWorkbookXmlTool.logAndExecute<string>({
requestId,
args: { datasourceName, publishedDatasourceId, datasourceCaption, revision, worksheetName },
callback: async () => {
const url = new URL(config.server);
const channel = (url.protocol === 'https:') ? 'https' : 'http';

Check failure on line 162 in src/tools/workbooks/generateWorkbookXml.ts

View workflow job for this annotation

GitHub Actions / build (>=22.7.5 <23)

Replace `(url.protocol·===·'https:')` with `url.protocol·===·'https:'`
const defaultPort = channel === 'https' ? '443' : '80';
const port = url.port && url.port !== '0' ? url.port : defaultPort;
const siteName = config.siteName ?? '';

const finalCaption = datasourceCaption?.trim() || datasourceName;
const finalRevision = (revision ?? '1.0').trim();
const finalWorksheetName = (worksheetName ?? 'Sheet 1').trim();

const xml = buildWorkbookXml({
siteName,
hostname: url.hostname,
port,
channel,
datasourceName: sanitizeForId(datasourceName),
datasourceCaption: finalCaption,
publishedDatasourceId,
revision: finalRevision,
worksheetName: finalWorksheetName,
});

return new Ok(xml);
},
constrainSuccessResult: (generateWorkbookXml) => {
return {
type: 'success',
result: generateWorkbookXml,
};
},
});
},
});

return generateWorkbookXmlTool;
};

Check failure on line 197 in src/tools/workbooks/generateWorkbookXml.ts

View workflow job for this annotation

GitHub Actions / build (>=22.7.5 <23)

Delete `⏎⏎`

Loading
Loading