getDatasourceMetadata.ts•6.13 kB
import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
import { Err, Ok } from 'ts-results-es';
import { z } from 'zod';
import { getConfig } from '../../config.js';
import { useRestApi } from '../../restApiInstance.js';
import { GraphQLResponse } from '../../sdks/tableau/apis/metadataApi.js';
import { Server } from '../../server.js';
import { getTableauAuthInfo } from '../../server/oauth/getTableauAuthInfo.js';
import { getVizqlDataServiceDisabledError } from '../getVizqlDataServiceDisabledError.js';
import { resourceAccessChecker } from '../resourceAccessChecker.js';
import { Tool } from '../tool.js';
import { validateDatasourceLuid } from '../validateDatasourceLuid.js';
import {
combineFields,
FieldsResult,
simplifyReadMetadataResult,
} from './datasourceMetadataUtils.js';
export const getGraphqlQuery = (datasourceLuid: string): string => `
query datasourceFieldInfo {
publishedDatasources(filter: { luid: "${datasourceLuid}" }) {
name
description
owner {
name
}
fields {
name
isHidden
description
descriptionInherited {
attribute
value
}
fullyQualifiedName
__typename
... on AnalyticsField {
__typename
}
... on ColumnField {
dataCategory
role
dataType
defaultFormat
semanticRole
aggregation
aggregationParam
}
... on CalculatedField {
dataCategory
role
dataType
defaultFormat
semanticRole
aggregation
aggregationParam
formula
isAutoGenerated
hasUserReference
}
... on BinField {
dataCategory
role
dataType
formula
binSize
}
... on GroupField {
dataCategory
role
dataType
hasOther
}
... on CombinedSetField {
delimiter
combinationType
}
}
}
}`;
const paramsSchema = {
datasourceLuid: z.string().nonempty(),
};
export type GetDatasourceMetadataError =
| {
type: 'feature-disabled';
}
| {
type: 'datasource-not-allowed';
message: string;
};
export const getGetDatasourceMetadataTool = (server: Server): Tool<typeof paramsSchema> => {
const getDatasourceMetadataTool = new Tool({
server,
name: 'get-datasource-metadata',
description: `
This tool retrieves field metadata for a specified datasource by taking the basic, high level, metadata results from Tableau's VizQL Data Service and enriches them with additional context provided by Tableau's Metadata API.
The fields provided by this tool will contain properties such as name and dataType, but may also expose richer context such as descriptions, dataCategories, roles, etc.
This tool should be used for getting the metadata to ground the use of a tool that queries Tableau published data sources.
`,
paramsSchema,
annotations: {
title: 'Get Datasource Metadata',
readOnlyHint: true,
openWorldHint: false,
},
argsValidator: validateDatasourceLuid,
callback: async ({ datasourceLuid }, { requestId, authInfo }): Promise<CallToolResult> => {
const config = getConfig();
const query = getGraphqlQuery(datasourceLuid);
return await getDatasourceMetadataTool.logAndExecute<
FieldsResult,
GetDatasourceMetadataError
>({
requestId,
authInfo,
args: { datasourceLuid },
callback: async () => {
const isDatasourceAllowedResult = await resourceAccessChecker.isDatasourceAllowed({
datasourceLuid,
restApiArgs: { config, requestId, server },
});
if (!isDatasourceAllowedResult.allowed) {
return new Err({
type: 'datasource-not-allowed',
message: isDatasourceAllowedResult.message,
});
}
return await useRestApi({
config,
requestId,
server,
jwtScopes: ['tableau:content:read', 'tableau:viz_data_service:read'],
authInfo: getTableauAuthInfo(authInfo),
callback: async (restApi) => {
// Fetching metadata from VizQL Data Service API.
const readMetadataResult = await restApi.vizqlDataServiceMethods.readMetadata({
datasource: {
datasourceLuid,
},
});
if (readMetadataResult.isErr()) {
return Err({ type: 'feature-disabled' });
}
if (config.disableMetadataApiRequests) {
// Exit early since requests to the Tableau Metadata API are disabled.
return Ok(simplifyReadMetadataResult(readMetadataResult.value));
}
let listFieldsResult: GraphQLResponse;
try {
// Fetching metadata from Tableau Metadata API.
// Using try-catch here since requests could fail if the service is not enabled.
listFieldsResult = await restApi.metadataMethods.graphql(query);
} catch {
return Ok(simplifyReadMetadataResult(readMetadataResult.value));
}
// Combine the results from the VizQL Data Service API and the Tableau Metadata API.
return Ok(combineFields(readMetadataResult.value, listFieldsResult));
},
});
},
constrainSuccessResult: (fields) => {
return {
type: 'success',
result: fields,
};
},
getErrorText: (error: GetDatasourceMetadataError) => {
switch (error.type) {
case 'feature-disabled':
return getVizqlDataServiceDisabledError();
case 'datasource-not-allowed':
return error.message;
}
},
});
},
});
return getDatasourceMetadataTool;
};