Skip to main content
Glama
callback.ts6.11 kB
import { randomBytes } from 'crypto'; import express from 'express'; import { Err, Ok, Result } from 'ts-results-es'; import { fromError } from 'zod-validation-error'; import { getConfig } from '../../config.js'; import { RestApi } from '../../sdks/tableau/restApi.js'; import { getTokenResult } from '../../sdks/tableau-oauth/methods.js'; import { TableauAccessToken } from '../../sdks/tableau-oauth/types.js'; import { TABLEAU_CLOUD_SERVER_URL } from './provider.js'; import { callbackSchema } from './schemas.js'; import { AuthorizationCode, PendingAuthorization } from './types.js'; /** * OAuth Callback Handler * * Receives callback from Tableau OAuth after user authorization. * Exchanges code for tokens, generates MCP authorization * code, and redirects back to client with code. */ export function callback( app: express.Application, pendingAuthorizations: Map<string, PendingAuthorization>, authorizationCodes: Map<string, AuthorizationCode>, ): void { const config = getConfig(); app.get('/Callback', async (req, res) => { const result = callbackSchema.safeParse(req.query); if (!result.success) { res.status(400).json({ error: 'invalid_request', error_description: fromError(result.error).toString(), }); return; } const { error, code, state } = result.data; if (error) { res.status(400).json({ error: 'access_denied', error_description: 'User denied authorization', }); return; } try { // Parse state to get auth key and Tableau state const [authKey, tableauState] = state?.split(':') ?? []; const pendingAuth = pendingAuthorizations.get(authKey); if (!pendingAuth || pendingAuth.tableauState !== tableauState) { res.status(400).json({ error: 'invalid_request', error_description: 'Invalid state parameter', }); return; } const tokensResult = await exchangeAuthorizationCode({ server: config.server || TABLEAU_CLOUD_SERVER_URL, code: code ?? '', redirectUri: config.oauth.redirectUri, clientId: pendingAuth.tableauClientId, codeVerifier: pendingAuth.tableauCodeVerifier, }); if (tokensResult.isErr()) { res.status(400).json({ error: 'invalid_request', error_description: tokensResult.error, }); return; } const { accessToken, refreshToken, expiresInSeconds, originHost } = tokensResult.value; const originHostUrl = new URL(`https://${originHost}`); if (config.server) { const configServerUrl = new URL(config.server); if (originHostUrl.hostname !== configServerUrl.hostname) { // Not sure if this can actually happen but without returning an error here, // this would fail downstream when attempting to authenticate to the REST API. res.status(400).json({ error: 'invalid_request', error_description: `Invalid origin host: ${originHost}. Expected: ${new URL(config.server).hostname}`, }); return; } } const server = originHostUrl.toString(); const restApi = new RestApi(server); restApi.setCredentials(accessToken, 'unknown user id'); const sessionResult = await restApi.serverMethods.getCurrentServerSession(); if (sessionResult.isErr()) { if (sessionResult.error.type === 'unauthorized') { res.status(401).json({ error: 'unauthorized', error_description: `Unable to get the Tableau server session. Error: ${JSON.stringify(sessionResult.error)}`, }); } else { res.status(500).json({ error: 'server_error', error_description: 'Internal server error during authorization. Unable to get the Tableau server session. Contact your administrator.', }); } return; } // Generate authorization code const authorizationCode = randomBytes(32).toString('hex'); authorizationCodes.set(authorizationCode, { clientId: pendingAuth.clientId, redirectUri: pendingAuth.redirectUri, codeChallenge: pendingAuth.codeChallenge, user: sessionResult.value.user, server, tableauClientId: pendingAuth.tableauClientId, tokens: { accessToken, refreshToken, expiresInSeconds, }, expiresAt: Math.floor((Date.now() + config.oauth.authzCodeTimeoutMs) / 1000), }); // Clean up pendingAuthorizations.delete(authKey); // Redirect back to client with authorization code const redirectUrl = new URL(pendingAuth.redirectUri); redirectUrl.searchParams.set('code', authorizationCode); redirectUrl.searchParams.set('state', pendingAuth.state); res.redirect(redirectUrl.toString()); } catch (error) { console.error('OAuth callback error:', error); res.status(500).json({ error: 'server_error', error_description: 'Internal server error during authorization. Contact your administrator.', }); } }); } /** * Exchanges authorization code for Tableau access tokens * * @param server - Tableau server host * @param code - Authorization code * @param redirectUri - Redirect URI used in initial request * @param clientId - Client ID * @param codeVerifier - Code verifier * @returns token response with access_token and refresh_token */ async function exchangeAuthorizationCode({ server, code, redirectUri, clientId, codeVerifier, }: { server: string; code: string; redirectUri: string; clientId: string; codeVerifier: string; }): Promise<Result<TableauAccessToken, string>> { try { const result = await getTokenResult(server, { grant_type: 'authorization_code', code, redirect_uri: redirectUri, client_id: clientId, code_verifier: codeVerifier, }); return Ok(result); } catch { return Err('Failed to exchange authorization code'); } }

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