UNPKG

templui-mcp-server

Version:

A Model Context Protocol (MCP) server for TemplUI components, providing AI assistants with access to component source code, documentation, demos, and metadata.

272 lines 11.5 kB
import { Axios } from "axios"; import { logError, logWarning, logInfo, logDebug } from './logger.js'; // TemplUI repository constants const REPO_OWNER = 'templui'; const REPO_NAME = 'templui'; const REPO_BRANCH = 'main'; const COMPONENTS_PATH = 'internal/components'; const SHOWCASE_PATH = 'internal/ui/showcase'; // GitHub API client for accessing repository 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; TempluiMcpServer/1.0.0)", }, timeout: 30000, transformResponse: [(data) => { try { return JSON.parse(data); } catch { return data; } }], }); // GitHub Raw client for 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; TempluiMcpServer/1.0.0)", }, timeout: 30000, transformResponse: [(data) => data], // Return raw data }); export class GitHubClient { apiKey; constructor(apiKey) { this.apiKey = apiKey; if (apiKey) { githubApi.defaults.headers["Authorization"] = `Bearer ${apiKey}`; logInfo("GitHub API configured with token"); } else { logWarning("No GitHub API key provided. Rate limited to 60 requests/hour."); } } /** * Get the source code of a TemplUI component (.templ file) */ async getComponentSource(componentName) { const componentPath = `${COMPONENTS_PATH}/${componentName.toLowerCase()}/${componentName.toLowerCase()}.templ`; try { logDebug(`Fetching component source: ${componentPath}`); const response = await githubRaw.get(`/${componentPath}`); return this.addCommentsToTemplCode(response.data, componentName); } catch (error) { logError(`Failed to fetch component "${componentName}"`, error); throw new Error(`Component "${componentName}" not found in TemplUI repository`); } } /** * Get the JavaScript code for a component (if it exists) */ async getComponentJavaScript(componentName) { const jsPath = `${COMPONENTS_PATH}/${componentName.toLowerCase()}/${componentName.toLowerCase()}.js`; try { logDebug(`Fetching component JavaScript: ${jsPath}`); const response = await githubRaw.get(`/${jsPath}`); return this.addCommentsToJavaScript(response.data, componentName); } catch (error) { logDebug(`No JavaScript file found for component "${componentName}"`); return null; } } /** * Get showcase/demo code for a component */ async getComponentDemo(componentName) { try { // Get all showcase files for this component const showcasePath = `repos/${REPO_OWNER}/${REPO_NAME}/contents/${SHOWCASE_PATH}`; const response = await githubApi.get(showcasePath); if (!response.data || !Array.isArray(response.data)) { throw new Error('Invalid response from GitHub API'); } // Filter files that match the component name pattern const componentFiles = response.data.filter((item) => item.type === 'file' && item.name.includes(componentName.toLowerCase()) && item.name.endsWith('.templ')); if (componentFiles.length === 0) { throw new Error(`No demo files found for component "${componentName}"`); } // Fetch content of each demo file const demos = []; for (const file of componentFiles) { try { const demoResponse = await githubRaw.get(`/${SHOWCASE_PATH}/${file.name}`); const demoWithComments = this.addCommentsToTemplCode(demoResponse.data, `${componentName} Demo: ${file.name}`); demos.push(demoWithComments); } catch (error) { logWarning(`Failed to fetch demo file: ${file.name}`); } } return demos; } catch (error) { logError(`Failed to fetch demos for component "${componentName}"`, error); throw new Error(`Demo files for component "${componentName}" not found`); } } /** * Get directory structure of components */ async getComponentsList() { try { const response = await githubApi.get(`/repos/${REPO_OWNER}/${REPO_NAME}/contents/${COMPONENTS_PATH}`); if (!response.data || !Array.isArray(response.data)) { throw new Error('Invalid response from GitHub API'); } const components = response.data .filter((item) => item.type === 'dir') .map((item) => item.name); if (components.length === 0) { throw new Error('No components found in the repository'); } logInfo(`Found ${components.length} components in repository`); return components; } catch (error) { logError('Error fetching components from GitHub API', error); throw new Error(`Failed to fetch components list: ${error.message}`); } } /** * Get latest commit information from the main branch */ async getLatestCommit() { try { logDebug('Fetching latest commit from main branch'); const response = await githubApi.get(`/repos/${REPO_OWNER}/${REPO_NAME}/commits/${REPO_BRANCH}`); if (!response.data || !response.data.sha) { throw new Error('Invalid commit response from GitHub API'); } return { sha: response.data.sha, date: response.data.commit.committer.date, message: response.data.commit.message }; } catch (error) { logError('Failed to fetch latest commit', error); throw new Error(`Failed to get latest commit: ${error instanceof Error ? error.message : String(error)}`); } } /** * Get components list from repository for dynamic discovery */ async getComponentsFromRepository() { try { logDebug('Fetching components from repository for dynamic discovery'); const response = await githubApi.get(`/repos/${REPO_OWNER}/${REPO_NAME}/contents/${COMPONENTS_PATH}`); if (!response.data || !Array.isArray(response.data)) { throw new Error('Invalid response from GitHub API'); } const components = response.data .filter((item) => item.type === 'dir') .map((item) => ({ name: item.name, path: item.path, type: 'component' })); logInfo(`Discovered ${components.length} components dynamically from repository`); return components; } catch (error) { logError('Failed to get components from repository', error); throw new Error(`Failed to discover components: ${error instanceof Error ? error.message : String(error)}`); } } /** * Get the directory structure of the repository */ async getDirectoryStructure(path = COMPONENTS_PATH) { try { const response = await githubApi.get(`/repos/${REPO_OWNER}/${REPO_NAME}/contents/${path}`); return response.data; } catch (error) { logError(`Failed to fetch directory structure for path: ${path}`, error); throw new Error(`Directory structure not found for path: ${path}`); } } /** * Add helpful comments to templ code */ addCommentsToTemplCode(code, componentName) { const lines = code.split('\n'); const commentedLines = []; // Add header comment commentedLines.push(`// Component: ${componentName}`); commentedLines.push(`// Framework: Go templ`); commentedLines.push(`// Source: github.com/templui/templui/internal/components`); commentedLines.push('//'); let insideType = false; let insideTempl = false; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const trimmedLine = line.trim(); // Add comments for type definitions if (trimmedLine.startsWith('type') && trimmedLine.includes('struct')) { commentedLines.push(`// Props structure defining component configuration`); insideType = true; } // Add comments for templ functions if (trimmedLine.startsWith('templ ') && trimmedLine.includes('(')) { commentedLines.push(`// Main component template - renders the ${componentName} component`); insideTempl = true; } // Add comments for methods if (trimmedLine.startsWith('func ') && trimmedLine.includes('Classes()')) { commentedLines.push(`// Helper method for generating CSS classes based on component variants`); } commentedLines.push(line); // Add explanatory comments for key sections if (insideType && trimmedLine.includes('Variant')) { commentedLines.push(' // Visual style variant (e.g., primary, secondary, outline)'); } if (insideType && trimmedLine.includes('Size')) { commentedLines.push(' // Component size (e.g., sm, default, lg)'); } if (insideType && trimmedLine.includes('Class')) { commentedLines.push(' // Additional CSS classes for customization'); } if (trimmedLine === '}' && insideType) { insideType = false; } } return commentedLines.join('\n'); } /** * Add helpful comments to JavaScript code */ addCommentsToJavaScript(code, componentName) { const lines = code.split('\n'); const commentedLines = []; // Add header comment commentedLines.push(`// JavaScript for ${componentName} component`); commentedLines.push(`// Provides client-side interactivity and behavior`); commentedLines.push(`// Framework: Vanilla JavaScript (no dependencies)`); commentedLines.push('//'); for (const line of lines) { const trimmedLine = line.trim(); // Add comments for key patterns if (trimmedLine.includes('addEventListener')) { commentedLines.push(' // Event listener for user interactions'); } if (trimmedLine.includes('querySelector')) { commentedLines.push(' // DOM element selection'); } if (trimmedLine.includes('data-') && trimmedLine.includes('getAttribute')) { commentedLines.push(' // Reading configuration from data attributes'); } commentedLines.push(line); } return commentedLines.join('\n'); } } //# sourceMappingURL=github-client.js.map