@hakxel/mantine-ui-server
Version:
MCP server for working with Mantine UI components - provides documentation, generation, and theme utilities
216 lines (215 loc) • 9.26 kB
JavaScript
/**
* Documentation fetcher for Mantine components
* Fetches documentation from mantine.dev website
*/
import axios from 'axios';
import puppeteer from 'puppeteer';
import * as cheerio from 'cheerio';
import { getConfig } from '../utils/config.js';
import { getCache, setCache } from '../utils/cache.js';
// Base URL for Mantine documentation
const MANTINE_DOCS_BASE_URL = 'https://mantine.dev';
/**
* Gets the documentation for a Mantine component
* @param componentName Name of the Mantine component
* @param forceRefresh Whether to bypass the cache and fetch fresh documentation
* @returns Component documentation
*/
export async function getComponentDocumentation(componentName, forceRefresh = false) {
const config = getConfig();
const cacheKey = `component_doc_${componentName}_${config.mantineVersion}`;
// Check cache first if not forcing refresh
if (!forceRefresh && config.caching?.documentation) {
const cachedDoc = getCache(cacheKey, config.caching.documentation, config.mantineVersion || '7.16.2');
if (cachedDoc) {
return cachedDoc;
}
}
// If not in cache or forcing refresh, fetch from website
const componentDoc = await fetchComponentDocFromWebsite(componentName);
// Cache the result
if (config.caching?.documentation) {
setCache(cacheKey, componentDoc, config.caching.documentation, config.mantineVersion || '7.16.2');
}
return componentDoc;
}
/**
* Fetches component documentation from the Mantine website
* @param componentName Name of the component
* @returns Component documentation
*/
async function fetchComponentDocFromWebsite(componentName) {
// Normalize component name
const normalizedName = componentName.charAt(0).toUpperCase() + componentName.slice(1);
// Determine the URL path for the component
const componentUrl = `${MANTINE_DOCS_BASE_URL}/core/${normalizedName.toLowerCase()}`;
try {
// Use Puppeteer to render the page with JavaScript
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const page = await browser.newPage();
await page.goto(componentUrl, { waitUntil: 'networkidle2' });
// Wait for important content to load
await page.waitForSelector('.mantine-Code-root', { timeout: 5000 }).catch(() => { });
// Get the HTML content
const content = await page.content();
// Close the browser
await browser.close();
// Parse the HTML using Cheerio
const $ = cheerio.load(content);
// Extract component description
const description = $('.mantine-Title-root').next('p').text().trim();
// Extract props table data
const props = [];
$('.mantine-Table-root tbody tr').each((i, elem) => {
const columns = $(elem).find('td');
if (columns.length >= 4) {
props.push({
name: $(columns[0]).text().trim(),
type: $(columns[1]).text().trim(),
defaultValue: $(columns[2]).text().trim() || undefined,
description: $(columns[3]).text().trim(),
required: $(columns[0]).text().includes('*')
});
}
});
// Extract code examples
const examples = [];
$('.mantine-Code-root').each((i, elem) => {
const codeBlock = $(elem);
const title = codeBlock.prev('h2, h3').text().trim() || `Example ${i + 1}`;
const code = codeBlock.text().trim();
examples.push({
title,
code,
description: codeBlock.prev('p').text().trim() || undefined
});
});
// Get import statement (from first example usually)
let importStatement = '';
if (examples.length > 0) {
const importRegex = /import\s+{\s*[^}]*}\s+from\s+['"]@mantine\/[^'"]+['"]/;
const match = examples[0].code.match(importRegex);
importStatement = match ? match[0] : `import { ${normalizedName} } from '@mantine/core'`;
}
else {
importStatement = `import { ${normalizedName} } from '@mantine/core'`;
}
// Determine package name from import statement
const packageRegex = /@mantine\/([a-z-]+)/;
const packageMatch = importStatement.match(packageRegex);
const packageName = packageMatch ? `@mantine/${packageMatch[1]}` : '@mantine/core';
// Extract related components
const relatedComponents = [];
$('.mantine-Anchor-root').each((i, elem) => {
const href = $(elem).attr('href');
if (href && href.startsWith('/core/') && !href.endsWith(normalizedName.toLowerCase())) {
const relatedComponent = href.split('/').pop();
if (relatedComponent) {
relatedComponents.push(relatedComponent.charAt(0).toUpperCase() + relatedComponent.slice(1));
}
}
});
return {
name: normalizedName,
description,
props,
examples,
importStatement,
packageName,
version: getConfig().mantineVersion || '7.16.2',
url: componentUrl,
relatedComponents: Array.from(new Set(relatedComponents)), // Remove duplicates
lastFetchedAt: new Date()
};
}
catch (error) {
console.error(`Error fetching documentation for ${componentName}:`, error);
throw new Error(`Failed to fetch documentation for ${componentName}: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Searches for components that match a query
* @param query Search query
* @returns Array of matching component names
*/
export async function searchComponents(query) {
try {
const response = await axios.get(`${MANTINE_DOCS_BASE_URL}/api/search`, {
params: { query },
headers: {
'User-Agent': 'MantineMcpServer/1.0.0'
}
});
if (response.data && Array.isArray(response.data.results)) {
return response.data.results
.filter((result) => result.type === 'component')
.map((result) => result.title);
}
return [];
}
catch (error) {
console.error(`Error searching for components with query "${query}":`, error);
return [];
}
}
/**
* Gets a list of all Mantine components
* @returns Array of component names
*/
export async function listAllComponents() {
try {
// Try to get common components from Mantine site navigation
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const page = await browser.newPage();
await page.goto(`${MANTINE_DOCS_BASE_URL}/core/button`, { waitUntil: 'networkidle2' });
// Extract components from navigation
const componentNames = await page.evaluate(() => {
const navLinks = Array.from(document.querySelectorAll('a[href^="/core/"]'));
return navLinks.map(link => {
const href = link.getAttribute('href');
if (!href)
return null;
const componentName = href.split('/').pop();
if (!componentName)
return null;
return componentName.charAt(0).toUpperCase() + componentName.slice(1);
}).filter(Boolean);
});
await browser.close();
return componentNames;
}
catch (error) {
console.error('Error listing all components:', error);
// Fallback to a list of common components
return [
'Accordion', 'ActionIcon', 'Affix', 'Alert', 'Anchor',
'AppShell', 'AspectRatio', 'Autocomplete', 'Avatar',
'Badge', 'Blockquote', 'Box', 'Breadcrumbs', 'Burger', 'Button',
'Card', 'Carousel', 'Center', 'Checkbox', 'Chip', 'Code', 'Collapse',
'ColorInput', 'ColorPicker', 'Container',
'DateInput', 'DatePicker', 'DatePickerInput', 'DateTimePicker', 'Divider', 'Drawer', 'Dropzone',
'FileInput', 'Flex', 'FloatingActionButton', 'FocusTrap',
'Grid', 'Group',
'Highlight',
'Image', 'Indicator', 'Input', 'Indicator',
'JsonInput',
'Kbd',
'List', 'Loader',
'Mark', 'Menu', 'Modal', 'MultiSelect',
'Navbar', 'NativeSelect', 'Notification', 'NumberInput',
'Overlay',
'Pagination', 'Paper', 'PasswordInput', 'Pill', 'PinInput', 'Popover', 'Portal', 'Progress',
'Radio', 'RangeCalendar', 'RangeSlider', 'Rating', 'RingProgress',
'ScrollArea', 'SegmentedControl', 'Select', 'SimpleGrid', 'Skeleton', 'Slider', 'Space', 'Spoiler',
'Stack', 'Stepper', 'Switch',
'Table', 'Tabs', 'Text', 'Textarea', 'TextInput', 'ThemeIcon', 'Timeline', 'Title', 'Tooltip',
'UnstyledButton'
];
}
}