Skip to main content
Glama
grafana
by grafana
axios.ts28.3 kB
import { Axios } from "axios"; import fs from "fs"; import path from "path"; // Constants for the Grafana UI repository structure const REPO_OWNER = "grafana"; const REPO_NAME = "grafana"; const REPO_BRANCH = "main"; const GRAFANA_UI_BASE_PATH = "packages/grafana-ui/src"; const COMPONENTS_PATH = `${GRAFANA_UI_BASE_PATH}/components`; // Local repository configuration let localRepoPath: string | null = null; // GitHub API for accessing repository structure and metadata const githubApi = new Axios({ baseURL: "https://api.github.com", headers: { "Content-Type": "application/json", Accept: "application/vnd.github+json", "User-Agent": "Mozilla/5.0 (compatible; GrafanaUiMcpServer/1.0.0)", ...((process.env.GITHUB_PERSONAL_ACCESS_TOKEN || process.env.GITHUB_TOKEN) && { Authorization: `Bearer ${process.env.GITHUB_PERSONAL_ACCESS_TOKEN || process.env.GITHUB_TOKEN}`, }), }, timeout: 30000, // Increased from 15000 to 30000 (30 seconds) transformResponse: [ (data) => { try { return JSON.parse(data); } catch { return data; } }, ], }); // GitHub Raw for directly fetching file contents const githubRaw = new Axios({ baseURL: `https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}/${REPO_BRANCH}`, headers: { "User-Agent": "Mozilla/5.0 (compatible; GrafanaUiMcpServer/1.0.0)", }, timeout: 30000, // Increased from 15000 to 30000 (30 seconds) transformResponse: [(data) => data], // Return raw data }); /** * Set local Grafana repository path * @param repoPath Path to local Grafana repository */ function setLocalGrafanaRepo(repoPath: string): void { // Validate path exists and has expected structure const componentsPath = path.join(repoPath, COMPONENTS_PATH); if (!fs.existsSync(componentsPath)) { throw new Error( `Invalid Grafana repository path: ${componentsPath} not found. ` + `Expected Grafana repository structure with ${COMPONENTS_PATH} directory.` ); } // Additional validation - check for at least one component directory try { const componentDirs = fs.readdirSync(componentsPath, { withFileTypes: true }) .filter(dirent => dirent.isDirectory()) .map(dirent => dirent.name); if (componentDirs.length === 0) { throw new Error( `No component directories found in ${componentsPath}. ` + `Expected Grafana UI component structure.` ); } } catch (error: any) { throw new Error( `Cannot read components directory ${componentsPath}: ${error.message}` ); } localRepoPath = repoPath; console.log(`Local Grafana repository configured: ${repoPath}`); } /** * Get component source from local filesystem * @param componentName Name of the component * @returns Promise with component source code or null if not found locally */ async function getComponentSourceLocal(componentName: string): Promise<string | null> { if (!localRepoPath) return null; const componentPath = path.join(localRepoPath, COMPONENTS_PATH, componentName, `${componentName}.tsx`); try { return fs.readFileSync(componentPath, 'utf8'); } catch (error) { return null; // Fall back to GitHub API } } /** * Fetch component source code from Grafana UI * @param componentName Name of the component (e.g., "Button", "Alert") * @returns Promise with component source code */ async function getComponentSource(componentName: string): Promise<string> { // Try local filesystem first const localSource = await getComponentSourceLocal(componentName); if (localSource !== null) { return localSource; } // Fall back to GitHub API const componentPath = `${COMPONENTS_PATH}/${componentName}/${componentName}.tsx`; try { const response = await githubRaw.get(`/${componentPath}`); return response.data; } catch (error) { throw new Error( `Component "${componentName}" not found in ${localRepoPath ? 'local repository or ' : ''}Grafana UI repository`, ); } } /** * Get component demo from local filesystem * @param componentName Name of the component * @returns Promise with component demo code or null if not found locally */ async function getComponentDemoLocal(componentName: string): Promise<string | null> { if (!localRepoPath) return null; const storyPath = path.join(localRepoPath, COMPONENTS_PATH, componentName, `${componentName}.story.tsx`); try { return fs.readFileSync(storyPath, 'utf8'); } catch (error) { return null; // Fall back to GitHub API } } /** * Fetch component story/example from Grafana UI * @param componentName Name of the component * @returns Promise with component story code */ async function getComponentDemo(componentName: string): Promise<string> { // Try local filesystem first const localDemo = await getComponentDemoLocal(componentName); if (localDemo !== null) { return localDemo; } // Fall back to GitHub API const storyPath = `${COMPONENTS_PATH}/${componentName}/${componentName}.story.tsx`; try { const response = await githubRaw.get(`/${storyPath}`); return response.data; } catch (error) { throw new Error( `Story for component "${componentName}" not found in ${localRepoPath ? 'local repository or ' : ''}Grafana UI repository`, ); } } /** * Get available components from local filesystem * @returns Promise with list of component names or null if not available locally */ async function getAvailableComponentsLocal(): Promise<string[] | null> { if (!localRepoPath) return null; const componentsPath = path.join(localRepoPath, COMPONENTS_PATH); try { const items = fs.readdirSync(componentsPath, { withFileTypes: true }); return items .filter(item => item.isDirectory()) .map(item => item.name) .sort(); } catch (error) { return null; // Fall back to GitHub API } } /** * Fetch all available components from Grafana UI * @returns Promise with list of component names */ async function getAvailableComponents(): Promise<string[]> { // Try local filesystem first const localComponents = await getAvailableComponentsLocal(); if (localComponents !== null) { return localComponents; } // Fall back to GitHub API try { const response = await githubApi.get( `/repos/${REPO_OWNER}/${REPO_NAME}/contents/${COMPONENTS_PATH}`, ); return response.data .filter((item: any) => item.type === "dir") .map((item: any) => item.name); } catch (error) { throw new Error( `Failed to fetch available components from ${localRepoPath ? 'local repository or ' : ''}Grafana UI` ); } } /** * Get component metadata from local filesystem * @param componentName Name of the component * @returns Promise with component metadata or null if not available locally */ async function getComponentMetadataLocal(componentName: string): Promise<any | null> { if (!localRepoPath) return null; const componentPath = path.join(localRepoPath, COMPONENTS_PATH, componentName); try { const items = fs.readdirSync(componentPath, { withFileTypes: true }); const files = items .filter(item => item.isFile()) .map(item => item.name); // Basic metadata from file structure return { name: componentName, type: "grafana-ui-component", source: "local", files: files, hasImplementation: files.includes(`${componentName}.tsx`), hasStories: files.some((file) => file.endsWith(".story.tsx")), hasDocumentation: files.includes(`${componentName}.mdx`), hasTests: files.some((file) => file.endsWith(".test.tsx")), hasTypes: files.includes("types.ts"), hasUtils: files.includes("utils.ts"), hasStyles: files.includes("styles.ts"), totalFiles: files.length, }; } catch (error) { return null; // Fall back to GitHub API } } /** * Fetch component files and extract basic metadata from Grafana UI * @param componentName Name of the component * @returns Promise with component metadata */ async function getComponentMetadata(componentName: string): Promise<any> { // Try local filesystem first const localMetadata = await getComponentMetadataLocal(componentName); if (localMetadata !== null) { return localMetadata; } // Fall back to GitHub API try { // Get the component directory contents const response = await githubApi.get( `/repos/${REPO_OWNER}/${REPO_NAME}/contents/${COMPONENTS_PATH}/${componentName}`, ); if (!Array.isArray(response.data)) { return null; } const files = response.data.map((item: any) => item.name); // Basic metadata from file structure return { name: componentName, type: "grafana-ui-component", source: "github", files: files, hasImplementation: files.includes(`${componentName}.tsx`), hasStories: files.some((file) => file.endsWith(".story.tsx")), hasDocumentation: files.includes(`${componentName}.mdx`), hasTests: files.some((file) => file.endsWith(".test.tsx")), hasTypes: files.includes("types.ts"), hasUtils: files.includes("utils.ts"), hasStyles: files.includes("styles.ts"), totalFiles: files.length, }; } catch (error) { console.error(`Error getting metadata for ${componentName}:`, error); return null; } } /** * Recursively builds a directory tree structure from a GitHub repository * @param owner Repository owner * @param repo Repository name * @param path Path within the repository to start building the tree from * @param branch Branch name * @returns Promise resolving to the directory tree structure */ async function buildDirectoryTree( owner: string = REPO_OWNER, repo: string = REPO_NAME, path: string = COMPONENTS_PATH, branch: string = REPO_BRANCH, ): Promise<any> { try { const response = await githubApi.get( `/repos/${owner}/${repo}/contents/${path}?ref=${branch}`, ); if (!response.data) { throw new Error("No data received from GitHub API"); } const contents = response.data; // Handle different response types from GitHub API if (!Array.isArray(contents)) { // Check if it's an error response (like rate limit) if (contents.message) { if (contents.message.includes("rate limit exceeded")) { throw new Error( `GitHub API rate limit exceeded. ${contents.message} Consider setting GITHUB_PERSONAL_ACCESS_TOKEN or GITHUB_TOKEN environment variable for higher rate limits.`, ); } else if (contents.message.includes("Not Found")) { throw new Error( `Path not found: ${path}. The path may not exist in the repository.`, ); } else { throw new Error(`GitHub API error: ${contents.message}`); } } // If contents is not an array, it might be a single file if (contents.type === "file") { return { path: contents.path, type: "file", name: contents.name, url: contents.download_url, sha: contents.sha, }; } else { throw new Error( `Unexpected response type from GitHub API: ${JSON.stringify(contents)}`, ); } } // Build tree node for this level (directory with multiple items) const result: Record<string, any> = { path, type: "directory", children: {}, }; // Process each item for (const item of contents) { if (item.type === "file") { // Add file to this directory's children result.children[item.name] = { path: item.path, type: "file", name: item.name, url: item.download_url, sha: item.sha, }; } else if (item.type === "dir") { // Recursively process subdirectory (limit depth to avoid infinite recursion) if (path.split("/").length < 8) { try { const subTree = await buildDirectoryTree( owner, repo, item.path, branch, ); result.children[item.name] = subTree; } catch (error) { console.warn(`Failed to fetch subdirectory ${item.path}:`, error); result.children[item.name] = { path: item.path, type: "directory", error: "Failed to fetch contents", }; } } } } return result; } catch (error: any) { console.error(`Error building directory tree for ${path}:`, error); // Check if it's already a well-formatted error from above if ( error.message && (error.message.includes("rate limit") || error.message.includes("GitHub API error")) ) { throw error; } // Provide more specific error messages for HTTP errors if (error.response) { const status = error.response.status; const responseData = error.response.data; const message = responseData?.message || "Unknown error"; if (status === 404) { throw new Error( `Path not found: ${path}. The path may not exist in the repository.`, ); } else if (status === 403) { if (message.includes("rate limit")) { throw new Error( `GitHub API rate limit exceeded: ${message} Consider setting GITHUB_PERSONAL_ACCESS_TOKEN or GITHUB_TOKEN environment variable for higher rate limits.`, ); } else { throw new Error(`Access forbidden: ${message}`); } } else if (status === 401) { throw new Error( `Authentication failed. Please check your GITHUB_PERSONAL_ACCESS_TOKEN or GITHUB_TOKEN if provided.`, ); } else { throw new Error(`GitHub API error (${status}): ${message}`); } } throw error; } } /** * Provides a basic directory structure for Grafana UI components without API calls * This is used as a fallback when API rate limits are hit */ function getBasicGrafanaUIStructure(): any { return { path: COMPONENTS_PATH, type: "directory", note: "Basic structure provided due to API limitations", description: "Grafana UI components directory", children: { Button: { path: `${COMPONENTS_PATH}/Button`, type: "directory", description: "Button component with variants and sizes", files: [ "Button.tsx", "Button.mdx", "Button.story.tsx", "Button.test.tsx", ], }, Alert: { path: `${COMPONENTS_PATH}/Alert`, type: "directory", description: "Alert component for notifications", files: ["Alert.tsx", "Alert.mdx", "Alert.test.tsx"], }, Input: { path: `${COMPONENTS_PATH}/Input`, type: "directory", description: "Input components for forms", files: ["Input.tsx", "Input.mdx", "Input.story.tsx"], }, }, }; } /** * Enhanced buildDirectoryTree with fallback for rate limits */ async function buildDirectoryTreeWithFallback( owner: string = REPO_OWNER, repo: string = REPO_NAME, path: string = COMPONENTS_PATH, branch: string = REPO_BRANCH, ): Promise<any> { try { return await buildDirectoryTree(owner, repo, path, branch); } catch (error: any) { // If it's a rate limit error and we're asking for the default components path, provide fallback if ( error.message && error.message.includes("rate limit") && path === COMPONENTS_PATH ) { console.warn("Using fallback directory structure due to rate limit"); return getBasicGrafanaUIStructure(); } // Re-throw other errors throw error; } } /** * Get component documentation from local filesystem * @param componentName Name of the component * @returns Promise with component documentation or null if not found locally */ async function getComponentDocumentationLocal(componentName: string): Promise<string | null> { if (!localRepoPath) return null; const docPath = path.join(localRepoPath, COMPONENTS_PATH, componentName, `${componentName}.mdx`); try { return fs.readFileSync(docPath, 'utf8'); } catch (error) { return null; // Fall back to GitHub API } } /** * Fetch component documentation from Grafana UI * @param componentName Name of the component * @returns Promise with component MDX documentation */ async function getComponentDocumentation( componentName: string, ): Promise<string> { // Try local filesystem first const localDocs = await getComponentDocumentationLocal(componentName); if (localDocs !== null) { return localDocs; } // Fall back to GitHub API const docPath = `${COMPONENTS_PATH}/${componentName}/${componentName}.mdx`; try { const response = await githubRaw.get(`/${docPath}`); return response.data; } catch (error) { throw new Error( `Documentation for component "${componentName}" not found in ${localRepoPath ? 'local repository or ' : ''}Grafana UI repository`, ); } } /** * Get component files from Grafana UI directory * @param componentName Name of the component * @returns Promise with all component files */ async function getComponentFiles(componentName: string): Promise<any> { try { const response = await githubApi.get( `/repos/${REPO_OWNER}/${REPO_NAME}/contents/${COMPONENTS_PATH}/${componentName}`, ); if (!Array.isArray(response.data)) { throw new Error(`Component directory "${componentName}" not found`); } const componentFiles: any = { name: componentName, path: `${COMPONENTS_PATH}/${componentName}`, files: {}, }; // Fetch each file's content for (const item of response.data) { if (item.type === "file") { try { const fileResponse = await githubRaw.get(`/${item.path}`); componentFiles.files[item.name] = { name: item.name, content: fileResponse.data, size: fileResponse.data.length, path: item.path, }; } catch (error) { // If individual file fails, mark it as unavailable componentFiles.files[item.name] = { name: item.name, content: null, error: "Failed to fetch file content", path: item.path, }; } } } return componentFiles; } catch (error: any) { if (error.response?.status === 404) { throw new Error( `Component "${componentName}" not found in Grafana UI repository.`, ); } throw error; } } /** * Set or update GitHub API key for higher rate limits * @param apiKey GitHub Personal Access Token */ function setGitHubApiKey(apiKey: string): void { // Update the Authorization header for the GitHub API instance if (apiKey && apiKey.trim()) { (githubApi.defaults.headers as any)["Authorization"] = `Bearer ${apiKey.trim()}`; console.log("GitHub API key updated successfully"); } else { // Remove authorization header if empty key provided delete (githubApi.defaults.headers as any)["Authorization"]; console.log("GitHub API key removed - using unauthenticated requests"); } } /** * Get current GitHub API rate limit status * @returns Promise with rate limit information */ async function getGitHubRateLimit(): Promise<any> { try { const response = await githubApi.get("/rate_limit"); return response.data; } catch (error: any) { throw new Error(`Failed to get rate limit info: ${error.message}`); } } /** * Get component tests from local filesystem * @param componentName Name of the component * @returns Promise with component test code or null if not found locally */ async function getComponentTestsLocal(componentName: string): Promise<string | null> { if (!localRepoPath) return null; const testPath = path.join(localRepoPath, COMPONENTS_PATH, componentName, `${componentName}.test.tsx`); try { return fs.readFileSync(testPath, 'utf8'); } catch (error) { return null; // Fall back to GitHub API } } /** * Fetch component test files from Grafana UI * @param componentName Name of the component * @returns Promise with component test code */ async function getComponentTests(componentName: string): Promise<string> { // Try local filesystem first const localTests = await getComponentTestsLocal(componentName); if (localTests !== null) { return localTests; } // Fall back to GitHub API const testPath = `${COMPONENTS_PATH}/${componentName}/${componentName}.test.tsx`; try { const response = await githubRaw.get(`/${testPath}`); return response.data; } catch (error) { throw new Error( `Tests for component "${componentName}" not found in ${localRepoPath ? 'local repository or ' : ''}Grafana UI repository`, ); } } /** * Search components by name and description * @param query Search query string * @param includeDescription Whether to search in documentation content * @returns Promise with filtered component list */ async function searchComponents( query: string, includeDescription: boolean = false, ): Promise<any[]> { try { const components = await getAvailableComponents(); const queryLower = query.toLowerCase(); const filteredComponents = []; for (const component of components) { let matches = false; // Check component name if (component.toLowerCase().includes(queryLower)) { matches = true; } // Check description if requested if (!matches && includeDescription) { try { const metadata = await getComponentMetadata(component); if (metadata) { // Check if documentation exists and search in it if (metadata.hasDocumentation) { try { const docs = await getComponentDocumentation(component); if (docs.toLowerCase().includes(queryLower)) { matches = true; } } catch (error) { // Ignore documentation fetch errors for search } } } } catch (error) { // Ignore metadata fetch errors for search } } if (matches) { filteredComponents.push({ name: component, relevance: component.toLowerCase() === queryLower ? 1.0 : component.toLowerCase().startsWith(queryLower) ? 0.8 : 0.5, }); } } // Sort by relevance return filteredComponents.sort((a, b) => b.relevance - a.relevance); } catch (error) { throw new Error( `Failed to search components: ${error instanceof Error ? error.message : String(error)}`, ); } } /** * Get theme files from local filesystem * @param category Optional category filter * @returns Promise with theme files or null if not available locally */ async function getThemeFilesLocal(category?: string): Promise<any | null> { if (!localRepoPath) return null; const themePaths = [ "packages/grafana-ui/src/themes/light.ts", "packages/grafana-ui/src/themes/dark.ts", "packages/grafana-ui/src/themes/base.ts", "packages/grafana-ui/src/themes/default.ts", ]; const themeFiles: any = { category: category || "all", source: "local", themes: {}, }; let foundAny = false; for (const themePath of themePaths) { try { const fullPath = path.join(localRepoPath, themePath); const content = fs.readFileSync(fullPath, 'utf8'); const themeName = themePath.split("/").pop()?.replace(".ts", "") || "unknown"; themeFiles.themes[themeName] = content; foundAny = true; } catch (error) { // Theme file doesn't exist locally, skip it } } return foundAny ? themeFiles : null; } /** * Fetch Grafana theme files * @param category Optional category filter (colors, typography, spacing, etc.) * @returns Promise with theme file content */ async function getThemeFiles(category?: string): Promise<any> { // Try local filesystem first const localThemes = await getThemeFilesLocal(category); if (localThemes !== null) { return localThemes; } // Fall back to GitHub API const themePaths = [ "packages/grafana-ui/src/themes/light.ts", "packages/grafana-ui/src/themes/dark.ts", "packages/grafana-ui/src/themes/base.ts", "packages/grafana-ui/src/themes/default.ts", ]; const themeFiles: any = { category: category || "all", source: "github", themes: {}, }; for (const themePath of themePaths) { try { const response = await githubRaw.get(`/${themePath}`); const themeName = themePath.split("/").pop()?.replace(".ts", "") || "unknown"; themeFiles.themes[themeName] = response.data; } catch (error) { // Theme file doesn't exist, skip it console.warn(`Theme file not found: ${themePath}`); } } return themeFiles; } /** * Get component dependencies by analyzing imports * @param componentName Name of the component * @param deep Whether to analyze dependencies recursively * @returns Promise with dependency tree */ async function getComponentDependencies( componentName: string, deep: boolean = false, ): Promise<any> { try { const componentSource = await getComponentSource(componentName); // Extract imports from component source const importRegex = /import\s+.*?\s+from\s+['"]([@\w\/\-\.]+)['"]/g; const dependencies: any = { component: componentName, dependencies: { external: [], internal: [], grafanaUI: [], }, deep: deep, }; let match; while ((match = importRegex.exec(componentSource)) !== null) { const dep = match[1]; if (dep.startsWith("@grafana/ui")) { dependencies.dependencies.grafanaUI.push(dep); } else if (dep.startsWith("./") || dep.startsWith("../")) { dependencies.dependencies.internal.push(dep); } else if (!dep.startsWith("@/")) { dependencies.dependencies.external.push(dep); } } // Remove duplicates dependencies.dependencies.external = [ ...new Set(dependencies.dependencies.external), ]; dependencies.dependencies.internal = [ ...new Set(dependencies.dependencies.internal), ]; dependencies.dependencies.grafanaUI = [ ...new Set(dependencies.dependencies.grafanaUI), ]; // If deep analysis requested, analyze internal dependencies if (deep && dependencies.dependencies.internal.length > 0) { dependencies.deepDependencies = {}; for (const internalDep of dependencies.dependencies.internal) { try { // Convert relative path to component name const depComponentName = internalDep .replace(/^\.\//, "") .replace(/\.tsx?$/, ""); if (depComponentName && depComponentName !== componentName) { dependencies.deepDependencies[depComponentName] = await getComponentDependencies(depComponentName, false); } } catch (error) { // Ignore errors for individual dependencies dependencies.deepDependencies[internalDep] = { error: "Failed to analyze dependency", }; } } } return dependencies; } catch (error) { throw new Error( `Failed to analyze dependencies for component "${componentName}": ${error instanceof Error ? error.message : String(error)}`, ); } } export const axios = { githubRaw, githubApi, buildDirectoryTree: buildDirectoryTreeWithFallback, // Use fallback version by default buildDirectoryTreeWithFallback, getComponentSource, getComponentDemo, getAvailableComponents, getComponentMetadata, getComponentDocumentation, getComponentFiles, getComponentTests, searchComponents, getThemeFiles, getComponentDependencies, setGitHubApiKey, setLocalGrafanaRepo, getGitHubRateLimit, // Path constants for easy access paths: { REPO_OWNER, REPO_NAME, REPO_BRANCH, GRAFANA_UI_BASE_PATH, COMPONENTS_PATH, }, };

Latest Blog Posts

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/grafana/grafana-ui-mcp-server'

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