Skip to main content
Glama
restApiInstance.ts7.47 kB
import { RequestId } from '@modelcontextprotocol/sdk/types.js'; import { Config, getConfig } from './config.js'; import { log, shouldLogWhenLevelIsAtLeast } from './logging/log.js'; import { maskRequest, maskResponse } from './logging/secretMask.js'; import { AxiosResponseInterceptorConfig, ErrorInterceptor, getRequestInterceptorConfig, getResponseInterceptorConfig, RequestInterceptor, RequestInterceptorConfig, ResponseInterceptor, ResponseInterceptorConfig, } from './sdks/tableau/interceptors.js'; import { RestApi } from './sdks/tableau/restApi.js'; import { Server, userAgent } from './server.js'; import { TableauAuthInfo } from './server/oauth/schemas.js'; import { getExceptionMessage } from './utils/getExceptionMessage.js'; import invariant from './utils/invariant.js'; import { isAxiosError } from './utils/isAxiosError.js'; type JwtScopes = | 'tableau:viz_data_service:read' | 'tableau:content:read' | 'tableau:insight_definitions_metrics:read' | 'tableau:insight_metrics:read' | 'tableau:metric_subscriptions:read' | 'tableau:insights:read' | 'tableau:views:download'; const getNewRestApiInstanceAsync = async ( config: Config, requestId: RequestId, server: Server, jwtScopes: Set<JwtScopes>, authInfo?: TableauAuthInfo, ): Promise<RestApi> => { const tableauServer = config.server || authInfo?.server; invariant(tableauServer, 'Tableau server could not be determined'); const restApi = new RestApi(tableauServer, { requestInterceptor: [ getRequestInterceptor(server, requestId), getRequestErrorInterceptor(server, requestId), ], responseInterceptor: [ getResponseInterceptor(server, requestId), getResponseErrorInterceptor(server, requestId), ], }); if (config.auth === 'pat') { await restApi.signIn({ type: 'pat', patName: config.patName, patValue: config.patValue, siteName: config.siteName, }); } else if (config.auth === 'direct-trust') { await restApi.signIn({ type: 'direct-trust', siteName: config.siteName, username: getJwtSubClaim(config, authInfo), clientId: config.connectedAppClientId, secretId: config.connectedAppSecretId, secretValue: config.connectedAppSecretValue, scopes: jwtScopes, additionalPayload: getJwtAdditionalPayload(config, authInfo), }); } else { if (!authInfo?.accessToken || !authInfo?.userId) { throw new Error('Auth info is required when not signing in first.'); } restApi.setCredentials(authInfo.accessToken, authInfo.userId); } return restApi; }; export const useRestApi = async <T>({ config, requestId, server, callback, jwtScopes, authInfo, }: { config: Config; requestId: RequestId; server: Server; jwtScopes: Array<JwtScopes>; callback: (restApi: RestApi) => Promise<T>; authInfo?: TableauAuthInfo; }): Promise<T> => { const restApi = await getNewRestApiInstanceAsync( config, requestId, server, new Set(jwtScopes), authInfo, ); try { return await callback(restApi); } finally { if (config.auth !== 'oauth') { // Tableau REST sessions for 'pat' and 'direct-trust' are intentionally ephemeral. // Sessions for 'oauth' are not. Signing out would invalidate the session, // preventing the access token from being reused for subsequent requests. await restApi.signOut(); } } }; export const getRequestInterceptor = (server: Server, requestId: RequestId): RequestInterceptor => (request) => { request.headers['User-Agent'] = getUserAgent(server); logRequest(server, request, requestId); return request; }; export const getRequestErrorInterceptor = (server: Server, requestId: RequestId): ErrorInterceptor => (error, baseUrl) => { if (!isAxiosError(error) || !error.request) { log.error(server, `Request ${requestId} failed with error: ${getExceptionMessage(error)}`, { logger: 'rest-api', requestId, }); return; } const { request } = error; logRequest( server, { baseUrl, ...getRequestInterceptorConfig(request), }, requestId, ); }; export const getResponseInterceptor = (server: Server, requestId: RequestId): ResponseInterceptor => (response) => { logResponse(server, response, requestId); return response; }; export const getResponseErrorInterceptor = (server: Server, requestId: RequestId): ErrorInterceptor => (error, baseUrl) => { if (!isAxiosError(error) || !error.response) { log.error( server, `Response from request ${requestId} failed with error: ${getExceptionMessage(error)}`, { logger: 'rest-api', requestId }, ); return; } // The type for the AxiosResponse headers is complex and not directly assignable to that of the Axios response interceptor's. const { response } = error as { response: AxiosResponseInterceptorConfig }; logResponse( server, { baseUrl, ...getResponseInterceptorConfig(response), }, requestId, ); }; function logRequest(server: Server, request: RequestInterceptorConfig, requestId: RequestId): void { const config = getConfig(); const maskedRequest = config.disableLogMasking ? request : maskRequest(request); const url = new URL(maskedRequest.url ?? '', maskedRequest.baseUrl); if (request.params && Object.keys(request.params).length > 0) { url.search = new URLSearchParams(request.params).toString(); } const messageObj = { type: 'request', requestId, method: maskedRequest.method, url: url.toString(), ...(shouldLogWhenLevelIsAtLeast('debug') && { headers: maskedRequest.headers, data: maskedRequest.data, params: maskedRequest.params, }), } as const; log.info(server, messageObj, { logger: 'rest-api', requestId }); } function logResponse( server: Server, response: ResponseInterceptorConfig, requestId: RequestId, ): void { const config = getConfig(); const maskedResponse = config.disableLogMasking ? response : maskResponse(response); const url = new URL(maskedResponse.url ?? '', maskedResponse.baseUrl); if (response.request?.params && Object.keys(response.request.params).length > 0) { url.search = new URLSearchParams(response.request.params).toString(); } const messageObj = { type: 'response', requestId, url: url.toString(), status: maskedResponse.status, ...(shouldLogWhenLevelIsAtLeast('debug') && { headers: maskedResponse.headers, data: maskedResponse.data, }), } as const; log.info(server, messageObj, { logger: 'rest-api', requestId }); } function getUserAgent(server: Server): string { const userAgentParts = [userAgent]; if (server.clientInfo) { const { name, version } = server.clientInfo; if (name) { userAgentParts.push(version ? `(${name} ${version})` : `(${name})`); } } return userAgentParts.join(' '); } function getJwtSubClaim(config: Config, authInfo: TableauAuthInfo | undefined): string { return config.jwtSubClaim.replaceAll('{OAUTH_USERNAME}', authInfo?.username ?? ''); } function getJwtAdditionalPayload( config: Config, authInfo: TableauAuthInfo | undefined, ): Record<string, unknown> { const json = config.jwtAdditionalPayload.replaceAll('{OAUTH_USERNAME}', authInfo?.username ?? ''); return JSON.parse(json || '{}'); }

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