Skip to main content
Glama
grafana
by grafana
tools.ts18.1 kB
/** * Tools implementation for the Model Context Protocol (MCP) server. * * This file defines the tools that can be called by the AI model through the MCP protocol. * Each tool has a schema that defines its parameters and a handler function that implements its logic. * */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; import { axios } from "./utils/axios.js"; import { parseMDXContent } from "./utils/mdx-parser.js"; import { parseStoryMetadata, extractStoryExamples, } from "./utils/story-parser.js"; import { extractThemeTokens, extractThemeMetadata, filterTokensByCategory, } from "./utils/theme-extractor.js"; import { z } from "zod"; /** * Creates a standardized success response * @param data Data to include in the response * @returns Formatted response object */ function createSuccessResponse(data: any) { return { content: [ { type: "text" as const, text: typeof data === "string" ? data : JSON.stringify(data, null, 2), }, ], }; } /** * Creates a standardized error response * @param message Error message * @param code Error code * @returns Formatted error response */ function createErrorResponse( message: string, code: ErrorCode = ErrorCode.InternalError, ) { throw new McpError(code, message); } /** * Define an MCP server for our tools */ export const server = new McpServer({ name: "GrafanaUI Tools", version: "1.0.0", }); // Unified tool schema as raw shape for MCP server const unifiedToolSchemaRaw = { 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(), }; // Unified tool schema with validation for handler.ts 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", }, ); // Unified tool: grafana_ui - Single tool for all Grafana UI operations server.tool( "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)}`, ); } }, ); // Export tools for backward compatibility export const tools = { grafana_ui: { name: "grafana_ui", description: "Unified tool for accessing Grafana UI components, documentation, themes, and metadata", inputSchema: { type: "object", properties: { action: { type: "string", enum: [ "get_component", "get_demo", "list_components", "get_metadata", "get_directory", "get_documentation", "get_stories", "get_tests", "search", "get_theme_tokens", "get_dependencies", ], description: "The action to perform", }, componentName: { type: "string", description: 'Name of the Grafana UI component (e.g., "Button", "Alert")', }, query: { type: "string", description: "Search query string (required for search action)", }, includeDescription: { type: "boolean", description: "Whether to search in documentation content (default: false)", }, category: { type: "string", description: "Token category to filter by (colors, typography, spacing, shadows, etc.)", }, deep: { type: "boolean", description: "Whether to analyze dependencies recursively (default: false)", }, path: { type: "string", description: "Path within the repository (default: components directory)", }, owner: { type: "string", description: 'Repository owner (default: "grafana")', }, repo: { type: "string", description: 'Repository name (default: "grafana")', }, branch: { type: "string", description: 'Branch name (default: "main")', }, }, required: ["action"], }, }, }; // Export schema for use in handler.ts export { unifiedToolSchema }; // Export tool handlers for backward compatibility export const toolHandlers = { grafana_ui: async (params: any) => { try { switch (params.action) { case "get_component": const sourceCode = await axios.getComponentSource( params.componentName!, ); return createSuccessResponse(sourceCode); case "get_demo": const demoCode = await axios.getComponentDemo(params.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( params.componentName!, ); return createSuccessResponse(metadata); case "get_directory": const directoryTree = await axios.buildDirectoryTree( params.owner || axios.paths.REPO_OWNER, params.repo || axios.paths.REPO_NAME, params.path || axios.paths.COMPONENTS_PATH, params.branch || axios.paths.REPO_BRANCH, ); return createSuccessResponse(directoryTree); case "get_documentation": const mdxContent = await axios.getComponentDocumentation( params.componentName!, ); const parsedContent = parseMDXContent( params.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( params.componentName!, ); const storyMetadata = parseStoryMetadata( params.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( params.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: params.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( params.query!, params.includeDescription || false, ); return createSuccessResponse({ query: params.query, includeDescription: params.includeDescription || false, results: searchResults, totalResults: searchResults.length, }); case "get_theme_tokens": const themeFiles = await axios.getThemeFiles(params.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: params.category ? filterTokensByCategory(tokens, params.category) : tokens, }; } } return createSuccessResponse({ category: params.category || "all", themes: processedThemes, availableThemes: Object.keys(processedThemes), }); case "get_dependencies": const dependencies = await axios.getComponentDependencies( params.componentName!, params.deep || false, ); return createSuccessResponse(dependencies); default: throw new McpError( ErrorCode.InvalidParams, `Unknown action: ${params.action}`, ); } } catch (error) { if (error instanceof McpError) { throw error; } throw new McpError( ErrorCode.InternalError, `Failed to execute action "${params.action}": ${error instanceof Error ? error.message : String(error)}`, ); } }, };

Implementation Reference

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