Skip to main content
Glama

dbt-mcp

Official
by dbt-labs
App.tsx16.6 kB
import { useEffect, useMemo, useState, useRef } from "react"; import "./App.css"; import dbtLogoBLK from "../assets/dbt_logo BLK.svg"; import dbtLogoWHT from "../assets/dbt_logo WHT.svg"; type Project = { id: number; name: string; account_id: number; account_name: string; }; type DbtPlatformContext = { dev_environment: { id: number; name: string; deployment_type: string; } | null; prod_environment: { id: number; name: string; deployment_type: string; } | null; decoded_access_token: { decoded_claims: { sub: number; }; }; }; type FetchRetryOptions = { attempts?: number; delayMs?: number; backoffFactor?: number; timeoutMs?: number; retryOnResponse?: (response: Response) => boolean; }; function isAbortError(error: unknown): boolean { if (error instanceof DOMException) { return error.name === "AbortError"; } return error instanceof Error && error.name === "AbortError"; } function isNetworkError(error: unknown): boolean { if (error instanceof TypeError) { return true; } return error instanceof Error && error.name === "TypeError"; } function sleep(ms: number) { return new Promise<void>((resolve) => { setTimeout(resolve, ms); }); } async function fetchWithRetry( input: RequestInfo | URL, init?: RequestInit, options?: FetchRetryOptions ): Promise<Response> { const { attempts = 3, delayMs = 500, backoffFactor = 2, timeoutMs = 10000, retryOnResponse, } = options ?? {}; let currentDelay = delayMs; for (let attempt = 0; attempt < attempts; attempt++) { if (attempt > 0 && currentDelay > 0) { await sleep(currentDelay); currentDelay *= backoffFactor; } const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeoutMs); // Listen to existing signal if present if (init?.signal) { init.signal.addEventListener("abort", () => controller.abort(), { once: true, }); } try { const response = await fetch(input, { ...init, signal: controller.signal, }); clearTimeout(timeoutId); if ( retryOnResponse && retryOnResponse(response) && attempt < attempts - 1 ) { // Consume response body to free resources try { await response.arrayBuffer(); } catch { // Ignore - may already be consumed or reader locked } continue; } return response; } catch (error) { clearTimeout(timeoutId); if (isAbortError(error)) { throw error; } if (!isNetworkError(error)) { throw error; } if (attempt === attempts - 1) { throw error; } } } throw new Error("Failed to fetch after retries"); } function parseHash(): URLSearchParams { const hash = window.location.hash.startsWith("#") ? window.location.hash.slice(1) : window.location.hash; const query = hash.startsWith("?") ? hash.slice(1) : hash; return new URLSearchParams(query); } type OAuthResult = { status: string | null; error: string | null; errorDescription: string | null; }; function useOAuthResult(): OAuthResult { const params = useMemo(() => parseHash(), []); return { status: params.get("status"), error: params.get("error"), errorDescription: params.get("error_description"), }; } type CustomDropdownProps = { value: number | null; onChange: (value: string) => void; options: Project[]; placeholder: string; id: string; }; function CustomDropdown({ value, onChange, options, placeholder, id, }: CustomDropdownProps) { const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef<HTMLDivElement>(null); const triggerRef = useRef<HTMLButtonElement>(null); // Close dropdown when clicking outside useEffect(() => { function handleClickOutside(event: MouseEvent) { if ( dropdownRef.current && !dropdownRef.current.contains(event.target as Node) ) { setIsOpen(false); } } if (isOpen) { document.addEventListener("mousedown", handleClickOutside); return () => { document.removeEventListener("mousedown", handleClickOutside); }; } }, [isOpen]); // Handle keyboard navigation useEffect(() => { function handleKeyDown(event: KeyboardEvent) { if (!isOpen) { if ( event.key === "Enter" || event.key === " " || event.key === "ArrowDown" ) { event.preventDefault(); setIsOpen(true); } return; } if (event.key === "Escape") { setIsOpen(false); triggerRef.current?.focus(); } } if (triggerRef.current?.contains(document.activeElement)) { document.addEventListener("keydown", handleKeyDown); return () => { document.removeEventListener("keydown", handleKeyDown); }; } }, [isOpen]); const selectedProject = options.find((p) => p.id === value); const handleToggle = () => { setIsOpen(!isOpen); }; const handleOptionSelect = (project: Project) => { onChange(project.id.toString()); setIsOpen(false); triggerRef.current?.focus(); }; return ( <div className="custom-dropdown" ref={dropdownRef}> <button ref={triggerRef} id={id} type="button" className={`dropdown-trigger ${isOpen ? "open" : ""} ${ !selectedProject ? "placeholder" : "" }`} onClick={handleToggle} aria-haspopup="listbox" aria-expanded={isOpen} aria-labelledby={`${id}-label`} > {selectedProject ? ( <> <div className="option-primary">{selectedProject.name}</div> <div className="option-secondary"> {selectedProject.account_name} </div> </> ) : ( placeholder )} </button> {isOpen && ( <div ref={dropdownRef} className="dropdown-options" role="listbox" aria-labelledby={`${id}-label`} > {options.map((project) => ( <button key={`${project.account_id}-${project.id}`} type="button" className={`dropdown-option ${ project.id === value ? "selected" : "" }`} onClick={() => handleOptionSelect(project)} role="option" aria-selected={project.id === value} > <div className="option-primary">{project.name}</div> <div className="option-secondary">{project.account_name}</div> </button> ))} </div> )} </div> ); } export default function App() { const oauthResult = useOAuthResult(); const [responseText, setResponseText] = useState<string | null>(null); const [projects, setProjects] = useState<Project[]>([]); const [projectsError, setProjectsError] = useState<string | null>(null); const [loadingProjects, setLoadingProjects] = useState(false); const [selectedProjectId, setSelectedProjectId] = useState<number | null>( null ); const [dbtPlatformContext, setDbtPlatformContext] = useState<DbtPlatformContext | null>(null); const [continuing, setContinuing] = useState(false); const [shutdownComplete, setShutdownComplete] = useState(false); // Load available projects after OAuth success useEffect(() => { if (oauthResult.status !== "success") return; const abortController = new AbortController(); let cancelled = false; const loadProjects = async () => { setLoadingProjects(true); setProjectsError(null); try { const response = await fetchWithRetry( "/projects", { signal: abortController.signal }, { attempts: 3, delayMs: 400 } ); if (!response.ok) { throw new Error(`Failed to load projects (${response.status})`); } const data: Project[] = await response.json(); if (!cancelled) { setProjects(data); } } catch (err) { if (cancelled || isAbortError(err)) { return; } const msg = err instanceof Error ? err.message : String(err); setProjectsError(msg); } finally { if (!cancelled) { setLoadingProjects(false); } } }; loadProjects(); return () => { cancelled = true; abortController.abort(); }; }, [oauthResult.status]); // Fetch saved selected project on load after OAuth success useEffect(() => { if (oauthResult.status !== "success") return; const abortController = new AbortController(); let cancelled = false; (async () => { try { const res = await fetchWithRetry( "/dbt_platform_context", { signal: abortController.signal }, { attempts: 2, delayMs: 400 } ); if (!res.ok || cancelled) return; // if no config yet or server error, skip silently const data: DbtPlatformContext = await res.json(); if (!cancelled) { setDbtPlatformContext(data); } } catch (err) { if (isAbortError(err) || cancelled) { return; } // ignore other failures to keep UX consistent } })(); return () => { cancelled = true; abortController.abort(); }; }, [oauthResult.status]); const onContinue = async () => { if (continuing) return; setContinuing(true); setResponseText(null); try { const res = await fetchWithRetry( "/shutdown", { method: "POST" }, { attempts: 3, delayMs: 400 } ); const text = await res.text(); if (res.ok) { setShutdownComplete(true); window.close(); } else { setResponseText(text); } } catch (err) { setResponseText(String(err)); } finally { setContinuing(false); } }; const onSelectProject = async (projectIdStr: string) => { setDbtPlatformContext(null); const projectId = Number(projectIdStr); setSelectedProjectId(Number.isNaN(projectId) ? null : projectId); const project = projects.find((p) => p.id === projectId); if (!project) return; try { const res = await fetchWithRetry( "/selected_project", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ account_id: project.account_id, project_id: project.id, }), }, { attempts: 3, delayMs: 400 } ); if (res.ok) { const data = await res.json(); setDbtPlatformContext(data); } else { setResponseText(await res.text()); setDbtPlatformContext(null); } } catch (err) { setResponseText(String(err)); setDbtPlatformContext(null); } }; return ( <div className="app-container"> <div className="logo-container"> <img src={dbtLogoBLK} alt="dbt" className="logo logo-light" /> <img src={dbtLogoWHT} alt="dbt" className="logo logo-dark" /> </div> <div className="app-content"> <header className="app-header"> <h1>dbt Platform Setup</h1> <p>Configure your dbt Platform connection</p> </header> {oauthResult.status === "error" && ( <section className="error-section"> <div className="section-header"> <h2>Authentication Error</h2> <p>There was a problem during authentication</p> </div> <div className="error-details"> {oauthResult.error && ( <div className="error-item"> <strong>Error Code:</strong> <code className="error-code">{oauthResult.error}</code> </div> )} {oauthResult.errorDescription && ( <div className="error-item"> <strong>Description:</strong> <p className="error-description"> {decodeURIComponent(oauthResult.errorDescription)} </p> </div> )} <div className="error-actions"> <p> Please close this window and try again. If the problem persists, contact support. </p> </div> </div> </section> )} {oauthResult.status === "success" && !shutdownComplete && ( <section className="project-selection-section"> <div className="section-header"> <h2>Select a Project</h2> <p>Choose the dbt project you want to work with</p> </div> <div className="form-content"> {loadingProjects && ( <div className="loading-state"> <div className="spinner"></div> <span>Loading projects…</span> </div> )} {projectsError && ( <div className="error-state"> <strong>Error loading projects</strong> <p>{projectsError}</p> </div> )} {!loadingProjects && !projectsError && ( <div className="form-group"> <label htmlFor="project-select" className="form-label" id="project-select-label" > Available Projects </label> <CustomDropdown id="project-select" value={selectedProjectId} onChange={onSelectProject} options={projects} placeholder="Choose a project" /> </div> )} </div> </section> )} {dbtPlatformContext && !shutdownComplete && ( <section className="context-section"> <div className="section-header"> <h2>Current Configuration</h2> <p>Your dbt Platform context is ready</p> </div> <div className="context-details"> <div className="context-item"> <strong>User ID:</strong>{" "} {dbtPlatformContext.decoded_access_token?.decoded_claims.sub} </div> {dbtPlatformContext.dev_environment && ( <div className="context-item"> <strong>Development Environment:</strong> <div className="environment-details"> <span className="env-name"> {dbtPlatformContext.dev_environment.name} </span> </div> </div> )} {dbtPlatformContext.prod_environment && ( <div className="context-item"> <strong>Production Environment:</strong> <div className="environment-details"> <span className="env-name"> {dbtPlatformContext.prod_environment.name} </span> </div> </div> )} </div> </section> )} {dbtPlatformContext && !shutdownComplete && ( <div className="button-container"> <button onClick={onContinue} className="primary-button" disabled={selectedProjectId === null || continuing} > {continuing ? "Closing…" : "Continue"} </button> </div> )} {shutdownComplete && ( <section className="completion-section"> <div className="completion-card"> <h2>All Set!</h2> <p> Your dbt Platform setup has finished. This window can now be closed. </p> </div> </section> )} {responseText && ( <section className="response-section"> <div className="section-header"> <h3>Response</h3> </div> <pre className="response-text">{responseText}</pre> </section> )} </div> </div> ); }

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/dbt-labs/dbt-mcp'

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