Skip to main content
Glama
datasourceMetadataUtils.ts8.01 kB
import { z } from 'zod'; import { GraphQLResponse } from '../../sdks/tableau/apis/metadataApi.js'; import { MetadataResponse } from '../../sdks/tableau/apis/vizqlDataServiceApi.js'; export const fieldSchema = z .object({ name: z.string(), columnClass: z.string(), dataType: z.string().nullable(), defaultAggregation: z.string().nullable(), description: z.string().nullable(), descriptionInherited: z .array(z.object({ attribute: z.string(), value: z.string().nullable() }).nullable()) .nullable(), dataCategory: z.string().nullable(), role: z.string().nullable(), defaultFormat: z.string().nullable(), semanticRole: z.string().nullable(), aggregation: z.string().nullable(), aggregationParam: z.string().nullable(), formula: z.string().nullable(), isAutoGenerated: z.boolean().nullable(), hasUserReference: z.boolean().nullable(), binSize: z.number().nullable(), }) .partial(); export const parameterSchema = z .object({ name: z.string(), parameterType: z.string(), dataType: z.string().nullable(), value: z.union([z.number(), z.string(), z.boolean(), z.null()]), members: z.array(z.union([z.number(), z.string(), z.boolean(), z.null()])), min: z.number().nullable(), max: z.number().nullable(), step: z.number().nullable(), minDate: z.string().nullable(), maxDate: z.string().nullable(), periodValue: z.number().nullable(), periodType: z.string().nullable(), }) .partial(); export const fieldsResultSchema = z.object({ fields: z.array(fieldSchema), parameters: z.array(parameterSchema), }); type Parameter = z.infer<typeof parameterSchema>; type Field = z.infer<typeof fieldSchema>; export type FieldsResult = z.infer<typeof fieldsResultSchema>; export function simplifyReadMetadataResult(readMetadataResult: MetadataResponse): FieldsResult { const simplifiedResponse: FieldsResult = { fields: [], parameters: [], }; if (!readMetadataResult.data) { return simplifiedResponse; } // This is a simplified response that attempts to reduce tokens by // only including essential fields and renaming properties. for (const field of readMetadataResult.data) { const toPush: Field = { name: field.fieldCaption, dataType: field.dataType, columnClass: field.columnClass, }; if (field.defaultAggregation) { toPush.defaultAggregation = field.defaultAggregation; } if (field.formula) { toPush.formula = field.formula; } simplifiedResponse.fields.push(toPush); } // Populate parameters from readMetadata results. if (readMetadataResult.extraData?.parameters) { for (const parameter of readMetadataResult.extraData.parameters) { const toPush: Parameter = { name: parameter.parameterCaption, parameterType: parameter.parameterType, dataType: parameter.dataType, value: parameter.value, }; if (parameter.parameterType === 'LIST' && parameter.members) { toPush.members = parameter.members; } else if (parameter.parameterType === 'QUANTITATIVE_DATE') { toPush.minDate = parameter.minDate; toPush.maxDate = parameter.maxDate; toPush.periodValue = parameter.periodValue; toPush.periodType = parameter.periodType; } else if (parameter.parameterType === 'QUANTITATIVE_RANGE') { toPush.min = parameter.min; toPush.max = parameter.max; toPush.step = parameter.step; } simplifiedResponse.parameters.push(toPush); } } return simplifiedResponse; } export function combineFields( readMetadataResult: MetadataResponse, listFieldsResult: GraphQLResponse, ): FieldsResult { // Create a response object that combines field data from // readMetadata (VizQL Data Service API) and listFields (GraphQL Metadata API) results // to optimize for LLM accuracy and reduce tokens in response. const combinedFields: FieldsResult = { fields: [], parameters: [], }; if (!readMetadataResult.data) { if (listFieldsResult.data.publishedDatasources[0]?.fields.length) { // fallback to returning listFields results if we don't have any fields from readMetadata. for (const field of listFieldsResult.data.publishedDatasources[0].fields) { const toPush: Field = { name: field.name }; if (field.dataType) { toPush.dataType = field.dataType; } if (field.aggregation) { toPush.defaultAggregation = field.aggregation; } populateFieldWithAdditionalProperties(field, toPush); combinedFields.fields.push(toPush); } } return combinedFields; } // Only include fields from readMetadata results in our response object since only those can be used in queries. for (const field of readMetadataResult.data) { // only keeping essential field properties. const toPush: Field = { name: field.fieldCaption, dataType: field.dataType, columnClass: field.columnClass, }; if (field.defaultAggregation) { toPush.defaultAggregation = field.defaultAggregation; } if (field.formula) { toPush.formula = field.formula; } combinedFields.fields.push(toPush); } // Populate parameters from readMetadata results. if (readMetadataResult.extraData?.parameters) { for (const parameter of readMetadataResult.extraData.parameters) { const toPush: Parameter = { name: parameter.parameterCaption, parameterType: parameter.parameterType, dataType: parameter.dataType, value: parameter.value, }; if (parameter.parameterType === 'LIST' && parameter.members) { toPush.members = parameter.members; } else if (parameter.parameterType === 'QUANTITATIVE_DATE') { toPush.minDate = parameter.minDate; toPush.maxDate = parameter.maxDate; toPush.periodValue = parameter.periodValue; toPush.periodType = parameter.periodType; } else if (parameter.parameterType === 'QUANTITATIVE_RANGE') { toPush.min = parameter.min; toPush.max = parameter.max; toPush.step = parameter.step; } combinedFields.parameters.push(toPush); } } if (!listFieldsResult.data.publishedDatasources[0]?.fields.length) { return combinedFields; } // Of the fields in our response object, populate them with additional properties we get from listFields results. for (const field of combinedFields.fields) { const matchingListField = listFieldsResult.data.publishedDatasources[0].fields.find( (f) => f.name === field.name, ); if (matchingListField) { populateFieldWithAdditionalProperties(matchingListField, field); } } return combinedFields; } function populateFieldWithAdditionalProperties(sourceField: Field, targetField: Field): void { if (sourceField.description) { targetField.description = sourceField.description; } if (sourceField.descriptionInherited?.length) { targetField.descriptionInherited = sourceField.descriptionInherited; } if (sourceField.dataCategory) { targetField.dataCategory = sourceField.dataCategory; } if (sourceField.role) { targetField.role = sourceField.role; } if (sourceField.defaultFormat) { targetField.defaultFormat = sourceField.defaultFormat; } if (sourceField.formula) { targetField.formula = sourceField.formula; // Possibly null or undefined, only populate if true or false. if (sourceField.isAutoGenerated != undefined) { targetField.isAutoGenerated = sourceField.isAutoGenerated; } // Possibly null or undefined, only populate if true or false. if (sourceField.hasUserReference != undefined) { targetField.hasUserReference = sourceField.hasUserReference; } } // Possibly null or undefined, only populate if defined. if (sourceField.binSize != undefined) { targetField.binSize = sourceField.binSize; } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/datalabs89/tableau-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server