From 322b16ecc83687fd46905f616c65c7ac3702d674 Mon Sep 17 00:00:00 2001 From: amahmoud Date: Wed, 29 Oct 2025 20:56:59 -0700 Subject: [PATCH 1/3] Add tools to generate TWB with basic vizualization. --- README.md | 33 +++ src/tools/toolName.ts | 4 +- src/tools/tools.ts | 4 + src/tools/workbooks/generateWorkbookXml.ts | 215 +++++++++++++++ .../workbooks/injectVizIntoWorkbookXml.ts | 260 ++++++++++++++++++ 5 files changed, 515 insertions(+), 1 deletion(-) create mode 100644 src/tools/workbooks/generateWorkbookXml.ts create mode 100644 src/tools/workbooks/injectVizIntoWorkbookXml.ts diff --git a/README.md b/README.md index 7aa67e4c..36418ebc 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,39 @@ 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). +- `datasourceCaption` (optional): Caption in the workbook; defaults to `datasourceName`. +- `repositoryId` (optional): Identifier used in repository location/dbname; defaults to a sanitized `datasourceName`. +- `revision` (optional): Revision string; defaults to `1.0`. +- `worksheetName` (optional): The initial sheet name; defaults to `Sheet 1`. +- `savedCredentialsViewerId` (optional): If provided, sets `saved-credentials-viewerid` on the connection. + +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 `` block +- Adding `` for the specified fields +- Binding fields to the `` and `` 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 ``. +- `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 diff --git a/src/tools/toolName.ts b/src/tools/toolName.ts index aeb8a0a9..541a4e9f 100644 --- a/src/tools/toolName.ts +++ b/src/tools/toolName.ts @@ -5,6 +5,8 @@ export const toolNames = [ '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', @@ -28,7 +30,7 @@ export type ToolGroupName = (typeof toolGroupNames)[number]; 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'], view: ['list-views', 'get-view-data', 'get-view-image'], pulse: [ 'list-all-pulse-metric-definitions', diff --git a/src/tools/tools.ts b/src/tools/tools.ts index 7732e9b3..33cbcac4 100644 --- a/src/tools/tools.ts +++ b/src/tools/tools.ts @@ -12,6 +12,8 @@ import { getGetViewDataTool } from './views/getViewData.js'; 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 = [ @@ -25,6 +27,8 @@ export const toolFactories = [ getListPulseMetricSubscriptionsTool, getGeneratePulseMetricValueInsightBundleTool, getGetWorkbookTool, + getGenerateWorkbookXmlTool, + getInjectVizIntoWorkbookXmlTool, getGetViewDataTool, getGetViewImageTool, getListWorkbooksTool, diff --git a/src/tools/workbooks/generateWorkbookXml.ts b/src/tools/workbooks/generateWorkbookXml.ts new file mode 100644 index 00000000..c717c550 --- /dev/null +++ b/src/tools/workbooks/generateWorkbookXml.ts @@ -0,0 +1,215 @@ +import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +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(), + // Optional overrides; sensible defaults are derived from config and datasourceName + datasourceCaption: z.string().trim().nonempty().optional(), + repositoryId: z.string().trim().nonempty().optional(), + revision: z.string().trim().nonempty().default('1.0').optional(), + worksheetName: z.string().trim().nonempty().default('Sheet 1').optional(), + // Optional: provide a viewer id if saved credentials are desired; omitted if not provided + savedCredentialsViewerId: z.string().trim().nonempty().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, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +function generateSqlProxyConnectionName(): string { + // Tableau-generated names look like: sqlproxy. + 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, + repositoryId, + revision, + worksheetName, + savedCredentialsViewerId, +}: { + siteName: string; + hostname: string; + port: string; + channel: 'http' | 'https'; + datasourceName: string; + datasourceCaption: string; + repositoryId: string; + revision: string; + worksheetName: string; + savedCredentialsViewerId?: string; +}): string { + const connectionName = generateSqlProxyConnectionName(); + const uuid = randomUUID().toUpperCase(); + + const pathDatasources = siteName + ? `/t/${escapeXmlAttribute(siteName)}/datasources` + : `/datasources`; + const derivedFrom = `${siteName ? `/t/${escapeXmlAttribute(siteName)}` : ''}/datasources/${escapeXmlAttribute(repositoryId)}?rev=${escapeXmlAttribute(revision)}`; + const siteAttr = siteName ? ` site='${escapeXmlAttribute(siteName)}'` : ''; + const savedCredsAttr = savedCredentialsViewerId + ? ` saved-credentials-viewerid='${escapeXmlAttribute(savedCredentialsViewerId)}'` + : ''; + + return ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +