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
25 changes: 25 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import { ZodiosError } from '@zodios/core';
import { CorsOptions } from 'cors';
import { fromError, isZodErrorLike } from 'zod-validation-error';

import { RestApi } from './sdks/tableau/restApi.js';
import { ProductVersion } from './sdks/tableau/types/serverInfo.js';
import { isToolGroupName, isToolName, toolGroups, ToolName } from './tools/toolName.js';
import { isTransport, TransportName } from './transports.js';
import { getExceptionMessage } from './utils/getExceptionMessage.js';
import invariant from './utils/invariant.js';

const authTypes = ['pat', 'direct-trust'] as const;
type AuthType = (typeof authTypes)[number];

export class Config {
private static _serverVersion: ProductVersion | undefined;

auth: AuthType;
server: string;
transport: TransportName;
Expand Down Expand Up @@ -121,6 +128,24 @@ export class Config {
this.connectedAppSecretValue = secretValue ?? '';
this.jwtAdditionalPayload = jwtAdditionalPayload || '{}';
}

getTableauServerVersion = async (): Promise<ProductVersion> => {
if (!Config._serverVersion) {
const restApi = new RestApi(this.server);
try {
Config._serverVersion = (await restApi.serverMethods.getServerInfo()).productVersion;
} catch (error) {
const reason =
error instanceof ZodiosError && isZodErrorLike(error.cause)
? fromError(error.cause).toString()
: getExceptionMessage(error);

throw new Error(`Failed to get server version: ${reason}`);
}
}

return Config._serverVersion;
};
}

function validateServer(server: string): void {
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ async function startServer(): Promise<void> {
switch (config.transport) {
case 'stdio': {
const server = new Server();
server.registerTools();
await server.registerTools();
server.registerRequestHandlers();

const transport = new StdioServerTransport();
Expand Down
9 changes: 1 addition & 8 deletions src/restApiInstance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,9 @@ import {
useRestApi,
} from './restApiInstance.js';
import { AuthConfig } from './sdks/tableau/authConfig.js';
import RestApi from './sdks/tableau/restApi.js';
import { RestApi } from './sdks/tableau/restApi.js';
import { Server } from './server.js';

vi.mock('./sdks/tableau/restApi.js', () => ({
default: vi.fn().mockImplementation(() => ({
signIn: vi.fn().mockResolvedValue(undefined),
signOut: vi.fn().mockResolvedValue(undefined),
})),
}));

vi.mock('./logging/log.js', () => ({
log: {
info: vi.fn(),
Expand Down
2 changes: 1 addition & 1 deletion src/restApiInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
ResponseInterceptor,
ResponseInterceptorConfig,
} from './sdks/tableau/interceptors.js';
import RestApi from './sdks/tableau/restApi.js';
import { RestApi } from './sdks/tableau/restApi.js';
import { Server } from './server.js';
import { getExceptionMessage } from './utils/getExceptionMessage.js';
import { isAxiosError } from './utils/isAxiosError.js';
Expand Down
17 changes: 17 additions & 0 deletions src/sdks/tableau/apis/serverApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { makeApi, makeEndpoint, ZodiosEndpointDefinitions } from '@zodios/core';
import { z } from 'zod';

import { serverInfo } from '../types/serverInfo.js';

const getServerInfoEndpoint = makeEndpoint({
method: 'get',
path: '/serverinfo',
alias: 'getServerInfo',
description: 'Returns the version of Tableau Server and the supported version of the REST API.',
response: z.object({
serverInfo,
}),
});

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

import { serverApis } from '../apis/serverApi.js';
import { ServerInfo } from '../types/serverInfo.js';
import Methods from './methods.js';

/**
* Server methods of the Tableau Server REST API
*
* @export
* @class ServerMethods
* @link https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_server.htm
*/
export default class ServerMethods extends Methods<typeof serverApis> {
constructor(baseUrl: string) {
super(new Zodios(baseUrl, serverApis));
}

/**
* Returns the version of Tableau Server and the supported version of the REST API.
*
* @link https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_server.htm#get_server_info
*/
getServerInfo = async (): Promise<ServerInfo> => {
return (await this._apiClient.getServerInfo()).serverInfo;
};
}
13 changes: 12 additions & 1 deletion src/sdks/tableau/restApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import AuthenticationMethods, {
import DatasourcesMethods from './methods/datasourcesMethods.js';
import MetadataMethods from './methods/metadataMethods.js';
import PulseMethods from './methods/pulseMethods.js';
import ServerMethods from './methods/serverMethods.js';
import ViewsMethods from './methods/viewsMethods.js';
import VizqlDataServiceMethods from './methods/vizqlDataServiceMethods.js';
import WorkbooksMethods from './methods/workbooksMethods.js';
Expand All @@ -24,7 +25,7 @@ import { Credentials } from './types/credentials.js';
* @export
* @class RestApi
*/
export default class RestApi {
export class RestApi {
private _creds?: Credentials;
private readonly _host: string;
private readonly _baseUrl: string;
Expand All @@ -35,6 +36,7 @@ export default class RestApi {
private _datasourcesMethods?: DatasourcesMethods;
private _metadataMethods?: MetadataMethods;
private _pulseMethods?: PulseMethods;
private _serverMethods?: ServerMethods;
private _vizqlDataServiceMethods?: VizqlDataServiceMethods;
private _viewsMethods?: ViewsMethods;
private _workbooksMethods?: WorkbooksMethods;
Expand Down Expand Up @@ -116,6 +118,15 @@ export default class RestApi {
return this._pulseMethods;
}

get serverMethods(): ServerMethods {
if (!this._serverMethods) {
this._serverMethods = new ServerMethods(this._baseUrl);
this._addInterceptors(this._baseUrl, this._serverMethods.interceptors);
}

return this._serverMethods;
}

get vizqlDataServiceMethods(): VizqlDataServiceMethods {
if (!this._vizqlDataServiceMethods) {
const baseUrl = `${this._host}/api/v1/vizql-data-service`;
Expand Down
12 changes: 12 additions & 0 deletions src/sdks/tableau/types/serverInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { z } from 'zod';

export const serverInfo = z.object({
productVersion: z.object({
value: z.string(),
build: z.string(),
}),
restApiVersion: z.string(),
});

export type ServerInfo = z.infer<typeof serverInfo>;
export type ProductVersion = ServerInfo['productVersion'];
15 changes: 8 additions & 7 deletions src/server.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ZodObject } from 'zod';

import { exportedForTesting as serverExportedForTesting } from './server.js';
import { testProductVersion } from './testShared.js';
import { getQueryDatasourceTool } from './tools/queryDatasource/queryDatasource.js';
import { toolNames } from './tools/toolName.js';
import { toolFactories } from './tools/tools.js';
Expand All @@ -24,9 +25,9 @@ describe('server', () => {

it('should register tools', async () => {
const server = getServer();
server.registerTools();
await server.registerTools();

const tools = toolFactories.map((tool) => tool(server));
const tools = toolFactories.map((tool) => tool(server, testProductVersion));
for (const tool of tools) {
expect(server.tool).toHaveBeenCalledWith(
tool.name,
Expand All @@ -41,9 +42,9 @@ describe('server', () => {
it('should register tools filtered by includeTools', async () => {
process.env.INCLUDE_TOOLS = 'query-datasource';
const server = getServer();
server.registerTools();
await server.registerTools();

const tool = getQueryDatasourceTool(server);
const tool = getQueryDatasourceTool(server, testProductVersion);
expect(server.tool).toHaveBeenCalledWith(
tool.name,
tool.description,
Expand All @@ -56,9 +57,9 @@ describe('server', () => {
it('should register tools filtered by excludeTools', async () => {
process.env.EXCLUDE_TOOLS = 'query-datasource';
const server = getServer();
server.registerTools();
await server.registerTools();

const tools = toolFactories.map((tool) => tool(server));
const tools = toolFactories.map((tool) => tool(server, testProductVersion));
for (const tool of tools) {
if (tool.name === 'query-datasource') {
expect(server.tool).not.toHaveBeenCalledWith(
Expand Down Expand Up @@ -93,7 +94,7 @@ describe('server', () => {
];

for (const sentence of sentences) {
expect(() => server.registerTools()).toThrow(sentence);
expect(async () => await server.registerTools()).rejects.toThrow(sentence);
}
});

Expand Down
9 changes: 5 additions & 4 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@ export class Server extends McpServer {
this.version = serverVersion;
}

registerTools = (): void => {
registerTools = async (): Promise<void> => {
for (const {
name,
description,
paramsSchema,
annotations,
callback,
} of this._getToolsToRegister()) {
} of await this._getToolsToRegister()) {
this.tool(name, description, paramsSchema, annotations, callback);
}
};
Expand All @@ -52,10 +52,11 @@ export class Server extends McpServer {
});
};

private _getToolsToRegister = (): Array<Tool<any>> => {
private _getToolsToRegister = async (): Promise<Array<Tool<any>>> => {
const { includeTools, excludeTools } = getConfig();
const productVersion = await getConfig().getTableauServerVersion();

const tools = toolFactories.map((tool) => tool(this));
const tools = toolFactories.map((tool) => tool(this, productVersion));
const toolsToRegister = tools.filter((tool) => {
if (includeTools.length > 0) {
return includeTools.includes(tool.name);
Expand Down
2 changes: 1 addition & 1 deletion src/server/express.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export async function startExpressServer({
server.close();
});

server.registerTools();
await server.registerTools();
server.registerRequestHandlers();

await server.connect(transport);
Expand Down
15 changes: 15 additions & 0 deletions src/testSetup.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { testProductVersion } from './testShared.js';

vi.stubEnv('SERVER', 'https://my-tableau-server.com');
vi.stubEnv('SITE_NAME', 'tc25');
vi.stubEnv('PAT_NAME', 'sponge');
Expand All @@ -13,3 +15,16 @@ vi.mock('./server.js', async (importOriginal) => ({
},
})),
}));

vi.mock('./sdks/tableau/restApi.js', async (importOriginal) => ({
...(await importOriginal()),
RestApi: vi.fn().mockImplementation(() => ({
signIn: vi.fn().mockResolvedValue(undefined),
signOut: vi.fn().mockResolvedValue(undefined),
serverMethods: {
getServerInfo: vi.fn().mockResolvedValue({
productVersion: testProductVersion,
}),
},
})),
}));
6 changes: 6 additions & 0 deletions src/testShared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { ProductVersion } from './sdks/tableau/types/serverInfo.js';

export const testProductVersion = {
value: '2025.3.0',
build: '20253.25.0903.0012',
} satisfies ProductVersion;
1 change: 1 addition & 0 deletions src/tools/queryDatasource/descriptions/2025.3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const queryDatasourceToolDescription20253 = `Custom description for query-datasource tool for Tableau 2025.3`;
Loading
Loading