grafana_ui
Access Grafana UI components, documentation, themes, and metadata to build Grafana-compatible interfaces with React components, Storybook examples, and design tokens.
Instructions
Unified tool for accessing Grafana UI components, documentation, themes, and metadata
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| action | Yes | The action to perform | |
| componentName | No | Name of the Grafana UI component (e.g., "Button", "Alert") | |
| query | No | Search query string (required for search action) | |
| includeDescription | No | Whether to search in documentation content (default: false) | |
| category | No | Token category to filter by (colors, typography, spacing, shadows, etc.) | |
| deep | No | Whether to analyze dependencies recursively (default: false) | |
| path | No | Path within the repository (default: components directory) | |
| owner | No | Repository owner (default: "grafana") | |
| repo | No | Repository name (default: "grafana") | |
| branch | No | Branch name (default: "main") |
Implementation Reference
- src/tools.ts:142-318 (handler)Primary MCP server.tool registration and handler implementation for the 'grafana_ui' tool. Dispatches to specific actions using axios utilities for fetching Grafana UI components, demos, metadata, etc."grafana_ui", "Unified tool for accessing Grafana UI components, documentation, themes, and metadata", unifiedToolSchemaRaw, async (params) => { try { // Validate parameters based on action const validatedParams = unifiedToolSchema.parse(params); switch (validatedParams.action) { case "get_component": const sourceCode = await axios.getComponentSource( validatedParams.componentName!, ); return createSuccessResponse(sourceCode); case "get_demo": const demoCode = await axios.getComponentDemo( validatedParams.componentName!, ); return createSuccessResponse(demoCode); case "list_components": const components = await axios.getAvailableComponents(); return createSuccessResponse({ components: components.sort(), total: components.length, }); case "get_metadata": const metadata = await axios.getComponentMetadata( validatedParams.componentName!, ); if (!metadata) { throw new McpError( ErrorCode.InvalidRequest, `Metadata not found for component "${validatedParams.componentName}"`, ); } return createSuccessResponse(metadata); case "get_directory": const directoryTree = await axios.buildDirectoryTree( validatedParams.owner || axios.paths.REPO_OWNER, validatedParams.repo || axios.paths.REPO_NAME, validatedParams.path || axios.paths.COMPONENTS_PATH, validatedParams.branch || axios.paths.REPO_BRANCH, ); return createSuccessResponse(directoryTree); case "get_documentation": const mdxContent = await axios.getComponentDocumentation( validatedParams.componentName!, ); const parsedContent = parseMDXContent( validatedParams.componentName!, mdxContent, ); return createSuccessResponse({ title: parsedContent.title, sections: parsedContent.sections.map((section) => ({ title: section.title, level: section.level, content: section.content.substring(0, 500) + (section.content.length > 500 ? "..." : ""), examples: section.examples.length, })), totalExamples: parsedContent.examples.length, imports: parsedContent.imports, components: parsedContent.components, }); case "get_stories": const storyContent = await axios.getComponentDemo( validatedParams.componentName!, ); const storyMetadata = parseStoryMetadata( validatedParams.componentName!, storyContent, ); const examples = extractStoryExamples(storyContent); return createSuccessResponse({ component: storyMetadata.componentName, meta: storyMetadata.meta, totalStories: storyMetadata.totalStories, hasInteractiveStories: storyMetadata.hasInteractiveStories, examples: examples.slice(0, 5), rawStoryCode: storyContent.substring(0, 1000) + (storyContent.length > 1000 ? "..." : ""), }); case "get_tests": const testContent = await axios.getComponentTests( validatedParams.componentName!, ); const testDescriptions = []; const testRegex = /(describe|it|test)\s*\(\s*['`"]([^'`"]+)['`"]/g; let match; while ((match = testRegex.exec(testContent)) !== null) { testDescriptions.push({ type: match[1], description: match[2], }); } return createSuccessResponse({ component: validatedParams.componentName, testDescriptions: testDescriptions.slice(0, 10), totalTests: testDescriptions.filter( (t) => t.type === "it" || t.type === "test", ).length, testCode: testContent.substring(0, 2000) + (testContent.length > 2000 ? "..." : ""), }); case "search": const searchResults = await axios.searchComponents( validatedParams.query!, validatedParams.includeDescription || false, ); return createSuccessResponse({ query: validatedParams.query, includeDescription: validatedParams.includeDescription || false, results: searchResults, totalResults: searchResults.length, }); case "get_theme_tokens": const themeFiles = await axios.getThemeFiles( validatedParams.category, ); const processedThemes: any = {}; for (const [themeName, themeContent] of Object.entries( themeFiles.themes, )) { if (typeof themeContent === "string") { const tokens = extractThemeTokens(themeContent); const themeMetadata = extractThemeMetadata(themeContent); processedThemes[themeName] = { metadata: themeMetadata, tokens: validatedParams.category ? filterTokensByCategory(tokens, validatedParams.category) : tokens, }; } } return createSuccessResponse({ category: validatedParams.category || "all", themes: processedThemes, availableThemes: Object.keys(processedThemes), }); case "get_dependencies": const dependencies = await axios.getComponentDependencies( validatedParams.componentName!, validatedParams.deep || false, ); return createSuccessResponse(dependencies); default: throw new McpError( ErrorCode.InvalidParams, `Unknown action: ${validatedParams.action}`, ); } } catch (error) { if (error instanceof McpError) { throw error; } throw new McpError( ErrorCode.InternalError, `Failed to execute action "${(params as any).action}": ${error instanceof Error ? error.message : String(error)}`, ); } }, );
- src/tools.ts:88-138 (schema)Zod validation schema for 'grafana_ui' tool inputs, defining actions and conditional parameters.const unifiedToolSchema = z .object({ action: z.enum([ "get_component", "get_demo", "list_components", "get_metadata", "get_directory", "get_documentation", "get_stories", "get_tests", "search", "get_theme_tokens", "get_dependencies", ]), componentName: z.string().optional(), query: z.string().optional(), includeDescription: z.boolean().optional(), category: z.string().optional(), deep: z.boolean().optional(), path: z.string().optional(), owner: z.string().optional(), repo: z.string().optional(), branch: z.string().optional(), }) .refine( (data) => { // Validate required parameters based on action switch (data.action) { case "get_component": case "get_demo": case "get_metadata": case "get_documentation": case "get_stories": case "get_tests": case "get_dependencies": return !!data.componentName; case "search": return !!data.query; case "list_components": case "get_directory": case "get_theme_tokens": return true; default: return false; } }, { message: "Missing required parameters for the specified action", }, );
- src/handler.ts:168-175 (registration)Tool schema lookup in request handler, specifically registers schema for 'grafana_ui'.function getToolSchema(toolName: string): z.ZodType | undefined { try { switch (toolName) { case "grafana_ui": return unifiedToolSchema; default: return undefined; }
- src/handler.ts:42-44 (registration)Registers the list tools handler using exported tools from tools.ts which includes 'grafana_ui'.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: Object.values(tools), }));
- src/utils/cache.ts:185-480 (helper)Grafana UI specific cache utility used for caching API responses in tool operations.export class GrafanaUICache { private cache: Cache; // Cache TTL configurations for different types of data private static readonly TTL = { COMPONENT_LIST: 24 * 60 * 60 * 1000, // 24 hours - component list changes rarely COMPONENT_SOURCE: 12 * 60 * 60 * 1000, // 12 hours - source code changes occasionally COMPONENT_METADATA: 6 * 60 * 60 * 1000, // 6 hours - metadata changes occasionally COMPONENT_STORIES: 6 * 60 * 60 * 1000, // 6 hours - stories change occasionally COMPONENT_DOCS: 6 * 60 * 60 * 1000, // 6 hours - docs change occasionally DIRECTORY_STRUCTURE: 24 * 60 * 60 * 1000, // 24 hours - directory structure changes rarely RATE_LIMIT: 5 * 60 * 1000, // 5 minutes - rate limit info changes frequently PARSED_METADATA: 12 * 60 * 60 * 1000, // 12 hours - parsed metadata is expensive to compute }; constructor(cache: Cache) { this.cache = cache; } /** * Generate cache key for component source code */ componentSourceKey(componentName: string): string { return `component:${componentName}:source`; } /** * Generate cache key for component metadata */ componentMetadataKey(componentName: string): string { return `component:${componentName}:metadata`; } /** * Generate cache key for component stories */ componentStoriesKey(componentName: string): string { return `component:${componentName}:stories`; } /** * Generate cache key for component documentation */ componentDocsKey(componentName: string): string { return `component:${componentName}:docs`; } /** * Generate cache key for component files */ componentFilesKey(componentName: string): string { return `component:${componentName}:files`; } /** * Generate cache key for parsed component metadata */ componentParsedMetadataKey(componentName: string): string { return `component:${componentName}:parsed-metadata`; } /** * Generate cache key for component list */ componentListKey(): string { return "components:list"; } /** * Generate cache key for directory structure */ directoryStructureKey(path?: string): string { return `directory:${path || "components"}:structure`; } /** * Generate cache key for rate limit info */ rateLimitKey(): string { return "github:rate-limit"; } /** * Cache component source code */ async getOrFetchComponentSource( componentName: string, fetchFn: () => Promise<string>, ): Promise<string> { return this.cache.getOrFetch( this.componentSourceKey(componentName), fetchFn, GrafanaUICache.TTL.COMPONENT_SOURCE, ); } /** * Cache component metadata */ async getOrFetchComponentMetadata( componentName: string, fetchFn: () => Promise<any>, ): Promise<any> { return this.cache.getOrFetch( this.componentMetadataKey(componentName), fetchFn, GrafanaUICache.TTL.COMPONENT_METADATA, ); } /** * Cache component stories */ async getOrFetchComponentStories( componentName: string, fetchFn: () => Promise<string>, ): Promise<string> { return this.cache.getOrFetch( this.componentStoriesKey(componentName), fetchFn, GrafanaUICache.TTL.COMPONENT_STORIES, ); } /** * Cache component documentation */ async getOrFetchComponentDocs( componentName: string, fetchFn: () => Promise<string>, ): Promise<string> { return this.cache.getOrFetch( this.componentDocsKey(componentName), fetchFn, GrafanaUICache.TTL.COMPONENT_DOCS, ); } /** * Cache component files */ async getOrFetchComponentFiles( componentName: string, fetchFn: () => Promise<any>, ): Promise<any> { return this.cache.getOrFetch( this.componentFilesKey(componentName), fetchFn, GrafanaUICache.TTL.COMPONENT_METADATA, ); } /** * Cache parsed component metadata */ async getOrFetchParsedMetadata( componentName: string, fetchFn: () => Promise<any>, ): Promise<any> { return this.cache.getOrFetch( this.componentParsedMetadataKey(componentName), fetchFn, GrafanaUICache.TTL.PARSED_METADATA, ); } /** * Cache component list */ async getOrFetchComponentList( fetchFn: () => Promise<string[]>, ): Promise<string[]> { return this.cache.getOrFetch( this.componentListKey(), fetchFn, GrafanaUICache.TTL.COMPONENT_LIST, ); } /** * Cache directory structure */ async getOrFetchDirectoryStructure( path: string, fetchFn: () => Promise<any>, ): Promise<any> { return this.cache.getOrFetch( this.directoryStructureKey(path), fetchFn, GrafanaUICache.TTL.DIRECTORY_STRUCTURE, ); } /** * Cache rate limit info */ async getOrFetchRateLimit(fetchFn: () => Promise<any>): Promise<any> { return this.cache.getOrFetch( this.rateLimitKey(), fetchFn, GrafanaUICache.TTL.RATE_LIMIT, ); } /** * Invalidate all cache entries for a specific component */ invalidateComponent(componentName: string): void { const prefixes = [`component:${componentName}:`]; prefixes.forEach((prefix) => { this.cache.deleteByPrefix(prefix); }); } /** * Invalidate all component-related cache entries */ invalidateAllComponents(): void { this.cache.deleteByPrefix("component:"); this.cache.delete(this.componentListKey()); } /** * Get cache statistics */ getStats(): { totalItems: number; componentSourceCached: number; componentMetadataCached: number; componentStoriesCached: number; componentDocsCached: number; expiredItems: number; } { const totalItems = this.cache.size(); // Count different types of cached items let componentSourceCached = 0; let componentMetadataCached = 0; let componentStoriesCached = 0; let componentDocsCached = 0; // This is a simple approximation - in a real implementation, // we'd iterate through the cache keys to count by pattern const estimatedItemsPerType = Math.floor(totalItems / 4); componentSourceCached = estimatedItemsPerType; componentMetadataCached = estimatedItemsPerType; componentStoriesCached = estimatedItemsPerType; componentDocsCached = estimatedItemsPerType; const expiredItems = this.cache.clearExpired(); return { totalItems, componentSourceCached, componentMetadataCached, componentStoriesCached, componentDocsCached, expiredItems, }; } /** * Warm up cache with commonly used components */ async warmUp( commonComponents: string[], fetchFunctions: { getComponentSource: (name: string) => Promise<string>; getComponentMetadata: (name: string) => Promise<any>; getComponentStories: (name: string) => Promise<string>; getComponentDocs: (name: string) => Promise<string>; }, ): Promise<void> { const promises = commonComponents.flatMap((componentName) => [ this.getOrFetchComponentSource(componentName, () => fetchFunctions.getComponentSource(componentName), ), this.getOrFetchComponentMetadata(componentName, () => fetchFunctions.getComponentMetadata(componentName), ), this.getOrFetchComponentStories(componentName, () => fetchFunctions.getComponentStories(componentName), ), this.getOrFetchComponentDocs(componentName, () => fetchFunctions.getComponentDocs(componentName), ), ]); // Execute all requests in parallel, but catch errors to prevent one failure from stopping others await Promise.allSettled(promises); } } // Export a singleton instance for Grafana UI cache export const grafanaUICache = new GrafanaUICache(cache);