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
7 changes: 7 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@
}
},
"detail": "Run build:watch in background"
},
{
"label": "npm test",
"type": "npm",
"script": "test",
"group": "test",
"detail": "Run test"
}
]
}
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ The following MCP tools are currently implemented:
| list-fields | Fetches field metadata (name, description) for the specified datasource ([Metadata API][meta]) |
| query-datasource | Run a Tableau VizQL query ([VDS API][vds]) |
| read-metadata | Requests metadata for the specified data source ([VDS API][vds]) |
| list-flows | Retrieves a list of published Prep flows from a specified Tableau site ([REST API][query]) |

Note: The Tableau MCP project is currently in early development. As we continue to enhance and
refine the implementation, the available functionality and tools may evolve. We welcome feedback and
Expand Down
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 54 additions & 0 deletions src/sdks/tableau/apis/flowsApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { makeApi, makeEndpoint, ZodiosEndpointDefinitions } from '@zodios/core';
import { z } from 'zod';

import { flowSchema } from '../types/flow.js';
import { paginationSchema } from '../types/pagination.js';

const listFlowsRestEndpoint = makeEndpoint({
method: 'get',
path: '/sites/:siteId/flows',
alias: 'listFlows',
description:
'Returns a list of flows on the specified site. Supports filter, sort, page-size, and page-number as query parameters.',
parameters: [
{
name: 'siteId',
type: 'Path',
schema: z.string(),
},
{
name: 'filter',
type: 'Query',
schema: z.string().optional(),
description: 'Filter expression (e.g., name:eq:SalesFlow)',
},
{
name: 'sort',
type: 'Query',
schema: z.string().optional(),
description: 'Sort expression (e.g., createdAt:desc)',
},
{
name: 'page-size',
type: 'Query',
schema: z.number().optional(),
description:
'The number of items to return in one response. The minimum is 1. The maximum is 1000. The default is 100.',
},
{
name: 'page-number',
type: 'Query',
schema: z.number().optional(),
description: 'The offset for paging. The default is 1.',
},
],
response: z.object({
pagination: paginationSchema,
flows: z.object({
flow: z.optional(z.array(flowSchema)),
}),
}),
});

const flowsApi = makeApi([listFlowsRestEndpoint]);
export const flowsApis = [...flowsApi] as const satisfies ZodiosEndpointDefinitions;
45 changes: 45 additions & 0 deletions src/sdks/tableau/methods/flowsMethods.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Zodios } from '@zodios/core';

import { Flow, flowsApis } from '../apis/flowsApi.js';

Check failure on line 3 in src/sdks/tableau/methods/flowsMethods.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

Module '"../apis/flowsApi.js"' has no exported member 'Flow'.
import { Credentials } from '../types/credentials.js';
import { Pagination } from '../types/pagination.js';
import AuthenticatedMethods from './authenticatedMethods.js';

export default class FlowsMethods extends AuthenticatedMethods<typeof flowsApis> {
constructor(baseUrl: string, creds: Credentials) {
super(new Zodios(baseUrl, flowsApis), creds);
}

/**
* Returns a list of flows on the specified site.
* @link https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref.htm#query_flows_for_site
* @param siteId - The Tableau site ID
* @param filter - The filter expression (e.g., name:eq:SalesFlow)
* @param sort - The sort expression (e.g., createdAt:desc)
* @param pageSize - The number of items to return in one response. The minimum is 1. The maximum is 1000. The default is 100.
* @param pageNumber - The offset for paging. The default is 1.
*/
listFlows = async ({
siteId,
filter,
sort,
pageSize,
pageNumber,
}: {
siteId: string;
filter?: string;
sort?: string;
pageSize?: number;
pageNumber?: number;
}): Promise<{ pagination: Pagination; flows: Flow[] }> => {
const response = await this._apiClient.listFlows({
params: { siteId },
queries: { filter, sort, 'page-size': pageSize, 'page-number': pageNumber },
...this.authHeader,
});
return {
pagination: response.pagination,
flows: response.flows.flow ?? [],
};
};
}
10 changes: 10 additions & 0 deletions src/sdks/tableau/restApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import DatasourcesMethods from './methods/datasourcesMethods.js';
import MetadataMethods from './methods/metadataMethods.js';
import VizqlDataServiceMethods from './methods/vizqlDataServiceMethods.js';
import { Credentials } from './types/credentials.js';
import FlowsMethods from './methods/flowsMethods.js';

/**
* Interface for the Tableau REST APIs
Expand All @@ -27,6 +28,7 @@ export default class RestApi {
private _datasourcesMethods?: DatasourcesMethods;
private _metadataMethods?: MetadataMethods;
private _vizqlDataServiceMethods?: VizqlDataServiceMethods;
private _flowsMethods?: FlowsMethods;
private static _version = '3.24';

private _requestInterceptor?: [RequestInterceptor, ErrorInterceptor?];
Expand Down Expand Up @@ -86,6 +88,14 @@ export default class RestApi {
return this._vizqlDataServiceMethods;
}

get flowsMethods(): FlowsMethods {
if (!this._flowsMethods) {
this._flowsMethods = new FlowsMethods(this._baseUrl, this.creds);
this._addInterceptors(this._baseUrl, this._flowsMethods.interceptors);
}
return this._flowsMethods;
}

signIn = async (authConfig: AuthConfig): Promise<void> => {
const authenticationMethods = new AuthenticationMethods(this._baseUrl);
this._addInterceptors(this._baseUrl, authenticationMethods.interceptors);
Expand Down
22 changes: 22 additions & 0 deletions src/sdks/tableau/types/flow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { z } from 'zod';

import { flowParamsSchema } from '../types/flowParameter.js';
import { ownerSchema } from '../types/owner.js';
import { projectSchema } from '../types/project.js';
import { tagsSchema } from '../types/tag.js';

export const flowSchema = z.object({
id: z.string(),
name: z.string(),
description: z.string().optional(),
webpageUrl: z.string(),
fileType: z.string(),
createdAt: z.string(),
updatedAt: z.string(),
project: projectSchema,
owner: ownerSchema,
tags: tagsSchema.optional(),
parameters: flowParamsSchema.optional(),
});

export type Flow = z.infer<typeof flowSchema>;
24 changes: 24 additions & 0 deletions src/sdks/tableau/types/flowParameter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { z } from 'zod';

export const flowParameterSchema = z.object({
id: z.string(),
type: z.string(),
name: z.string(),
description: z.string().optional(),
value: z.string().optional(),
isRequired: z.coerce.boolean(),
domain: z.object({
domainType: z.string(),
values: z.object({
value: z.array(z.string()),
}).optional(),
}).optional(),
});

export type FlowParameter = z.infer<typeof flowParameterSchema>;

export const flowParamsSchema = z.object({
parameter: z.array(flowParameterSchema).optional(),
});

export type FlowParams = z.infer<typeof flowParamsSchema>;
7 changes: 7 additions & 0 deletions src/sdks/tableau/types/owner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { z } from 'zod';

export const ownerSchema = z.object({
id: z.string(),
});

export type Owner = z.infer<typeof ownerSchema>;
8 changes: 8 additions & 0 deletions src/sdks/tableau/types/project.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { z } from 'zod';

export const projectSchema = z.object({
name: z.string(),
id: z.string(),
});

export type Project = z.infer<typeof projectSchema>;
13 changes: 13 additions & 0 deletions src/sdks/tableau/types/tag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { z } from 'zod';

export const tagSchema = z.object({
label: z.string(),
});

export type Tag = z.infer<typeof tagSchema>;

export const tagsSchema = z.object({
tag: z.array(tagSchema).optional(),
});

export type Tags = z.infer<typeof tagSchema>;
29 changes: 29 additions & 0 deletions src/tools/listFlows/flowsFilterUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { parseAndValidateFlowFilterString } from './flowsFilterUtils.js';

describe('parseAndValidateFlowFilterString', () => {
it('should return the filter string if valid (single expression)', () => {
expect(parseAndValidateFlowFilterString('name:eq:SalesFlow')).toBe('name:eq:SalesFlow');
expect(parseAndValidateFlowFilterString('createdAt:gt:2023-01-01T00:00:00Z')).toBe('createdAt:gt:2023-01-01T00:00:00Z');
});

it('should return the filter string if valid (multiple expressions)', () => {
const filter = 'name:eq:SalesFlow,tags:in:tag1|tag2,createdAt:gte:2023-01-01T00:00:00Z';
expect(parseAndValidateFlowFilterString(filter)).toBe(filter);
});

it('should throw if field is not supported', () => {
expect(() => parseAndValidateFlowFilterString('foo:eq:bar')).toThrow('Unsupported filter field: foo');
});

it('should throw if operator is not supported', () => {
expect(() => parseAndValidateFlowFilterString('name:like:SalesFlow')).toThrow('Unsupported filter operator: like');
});

it('should throw if value is missing', () => {
expect(() => parseAndValidateFlowFilterString('name:eq')).toThrow('Missing value for filter: name:eq');
});

it('should return undefined if filter is undefined', () => {
expect(parseAndValidateFlowFilterString(undefined)).toBeUndefined();
});
});
27 changes: 27 additions & 0 deletions src/tools/listFlows/flowsFilterUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Filter validation utility for flows

const SUPPORTED_FIELDS = [
'name', 'tags', 'createdAt',
];
const SUPPORTED_OPERATORS = [
'eq', 'in', 'gt', 'gte', 'lt', 'lte',
];

export function parseAndValidateFlowFilterString(filter?: string): string | undefined {
if (!filter) return undefined;
// Simple validation example (extend as needed)
const expressions = filter.split(',');
for (const expr of expressions) {
const [field, operator, ...rest] = expr.split(':');
if (!SUPPORTED_FIELDS.includes(field)) {
throw new Error(`Unsupported filter field: ${field}`);
}
if (!SUPPORTED_OPERATORS.includes(operator)) {
throw new Error(`Unsupported filter operator: ${operator}`);
}
if (rest.length === 0) {
throw new Error(`Missing value for filter: ${expr}`);
}
}
return filter;
}
Loading
Loading