shadcn-svelte-mcp-server
Version:
A Model Context Protocol (MCP) server for shadcn-svelte components, providing AI assistants with access to component source code, demos, blocks, and metadata.
821 lines (820 loc) • 32.5 kB
JavaScript
import { Axios } from "axios";
import { logError, logWarning, logInfo } from './logger.js';
// Constants for the shadcn-svelte repository structure
const REPO_OWNER = 'huntabyte';
const REPO_NAME = 'shadcn-svelte';
const REPO_BRANCH = 'main';
const DOCS_BASE_PATH = 'docs/src/lib/registry';
const REGISTRY_UI_PATH = `${DOCS_BASE_PATH}/ui`;
const REGISTRY_EXAMPLES_PATH = `${DOCS_BASE_PATH}/examples`;
const REGISTRY_BLOCKS_PATH = `${DOCS_BASE_PATH}/blocks`;
const REGISTRY_HOOKS_PATH = `${DOCS_BASE_PATH}/hooks`;
const REGISTRY_LIB_PATH = `${DOCS_BASE_PATH}/lib`;
// 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; ShadcnSvelteMcpServer/1.0.0)",
...(process.env.GITHUB_PERSONAL_ACCESS_TOKEN && {
"Authorization": `Bearer ${process.env.GITHUB_PERSONAL_ACCESS_TOKEN}`
})
},
timeout: 30000,
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; ShadcnSvelteMcpServer/1.0.0)",
},
timeout: 30000,
transformResponse: [(data) => data], // Return raw data
});
/**
* Fetch component source code from the shadcn-svelte registry
* @param componentName Name of the component
* @returns Promise with component source code and structure
*/
async function getComponentSource(componentName) {
try {
const componentPath = `${REGISTRY_UI_PATH}/${componentName.toLowerCase()}`;
const response = await githubApi.get(`/repos/${REPO_OWNER}/${REPO_NAME}/contents/${componentPath}?ref=${REPO_BRANCH}`);
if (!response.data) {
throw new Error(`Component "${componentName}" not found`);
}
const componentStructure = {
name: componentName,
type: 'svelte-component',
files: {},
structure: [],
totalFiles: 0,
dependencies: new Set(),
imports: new Set()
};
if (Array.isArray(response.data)) {
componentStructure.totalFiles = response.data.length;
for (const item of response.data) {
if (item.type === 'file' && (item.name.endsWith('.svelte') || item.name.endsWith('.ts'))) {
const fileResponse = await githubRaw.get(`/${item.path}`);
const content = fileResponse.data;
const dependencies = extractDependencies(content);
const imports = extractImports(content);
componentStructure.files[item.name] = {
path: item.name,
content: content,
size: content.length,
lines: content.split('\n').length,
dependencies: dependencies,
imports: imports
};
dependencies.forEach((dep) => componentStructure.dependencies.add(dep));
imports.forEach((imp) => componentStructure.imports.add(imp));
componentStructure.structure.push({
name: item.name,
type: 'file',
size: content.length,
fileType: item.name.endsWith('.svelte') ? 'svelte' : 'typescript'
});
}
}
}
componentStructure.dependencies = Array.from(componentStructure.dependencies);
componentStructure.imports = Array.from(componentStructure.imports);
return componentStructure;
}
catch (error) {
if (error.response?.status === 404) {
throw new Error(`Component "${componentName}" not found in shadcn-svelte registry`);
}
throw error;
}
}
/**
* Fetch component demo/example from the shadcn-svelte registry
* @param componentName Name of the component
* @returns Promise with component demo code
*/
async function getComponentDemo(componentName) {
try {
const demoPatterns = [
`${REGISTRY_EXAMPLES_PATH}/${componentName.toLowerCase()}-demo.svelte`,
`${REGISTRY_EXAMPLES_PATH}/${componentName.toLowerCase()}.svelte`,
`${REGISTRY_EXAMPLES_PATH}/${componentName.toLowerCase()}-01.svelte`
];
for (const demoPath of demoPatterns) {
try {
const response = await githubRaw.get(`/${demoPath}`);
if (response.status === 200) {
return response.data;
}
}
catch (error) {
// Continue to next pattern
}
}
throw new Error(`Demo not found`);
}
catch (error) {
throw new Error(`Demo for component "${componentName}" not found in shadcn-svelte registry`);
}
}
/**
* Fetch all available components from the registry
* @returns Promise with list of component names
*/
async function getAvailableComponents() {
try {
const response = await githubApi.get(`/repos/${REPO_OWNER}/${REPO_NAME}/contents/${REGISTRY_UI_PATH}?ref=${REPO_BRANCH}`);
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 registry');
}
return components.sort();
}
catch (error) {
logError('Error fetching components from GitHub API', error);
if (error.response) {
const status = error.response.status;
const message = error.response.data?.message || 'Unknown error';
if (status === 403 && message.includes('rate limit')) {
throw new Error(`GitHub API rate limit exceeded. Please set GITHUB_PERSONAL_ACCESS_TOKEN environment variable for higher limits. Error: ${message}`);
}
else if (status === 404) {
throw new Error(`Components directory not found. The path ${REGISTRY_UI_PATH} may not exist in the repository.`);
}
else if (status === 401) {
throw new Error(`Authentication failed. Please check your GITHUB_PERSONAL_ACCESS_TOKEN if provided.`);
}
else {
throw new Error(`GitHub API error (${status}): ${message}`);
}
}
if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND' || error.code === 'ETIMEDOUT') {
throw new Error(`Network error: ${error.message}. Please check your internet connection.`);
}
logWarning('Using fallback component list due to API issues');
return getFallbackComponents();
}
}
/**
* Fallback list of known shadcn-svelte components
*/
function getFallbackComponents() {
return [
'accordion',
'alert',
'alert-dialog',
'aspect-ratio',
'avatar',
'badge',
'breadcrumb',
'button',
'calendar',
'card',
'carousel',
'checkbox',
'collapsible',
'command',
'context-menu',
'dialog',
'drawer',
'dropdown-menu',
'form',
'hover-card',
'input',
'input-otp',
'label',
'menubar',
'navigation-menu',
'pagination',
'popover',
'progress',
'radio-group',
'resizable',
'scroll-area',
'select',
'separator',
'sheet',
'skeleton',
'slider',
'sonner',
'switch',
'table',
'tabs',
'textarea',
'toggle',
'toggle-group',
'tooltip'
];
}
/**
* Fetch component metadata from the registry
* @param componentName Name of the component
* @returns Promise with component metadata
*/
async function getComponentMetadata(componentName) {
try {
// Try to get meta.json file from component directory
const metaPath = `${REGISTRY_UI_PATH}/${componentName.toLowerCase()}/meta.json`;
try {
const response = await githubRaw.get(`/${metaPath}`);
const metaData = JSON.parse(response.data);
return {
...metaData,
name: componentName,
type: 'registry:ui',
framework: 'svelte',
source: 'shadcn-svelte'
};
}
catch (error) {
// If no meta.json, return basic metadata
return {
name: componentName,
type: 'registry:ui',
framework: 'svelte',
dependencies: [],
registryDependencies: [],
source: 'shadcn-svelte'
};
}
}
catch (error) {
logError(`Error getting metadata for ${componentName}`, error);
return null;
}
}
/**
* Fetch block code from the shadcn-svelte blocks directory
* @param blockName Name of the block (e.g., "calendar-01", "dashboard-01")
* @param includeComponents Whether to include component files for complex blocks
* @returns Promise with block code and structure
*/
async function getBlockCode(blockName, includeComponents = true) {
try {
// First, check if it's a simple block file (.svelte)
try {
const simpleBlockResponse = await githubRaw.get(`/${REGISTRY_BLOCKS_PATH}/${blockName}.svelte`);
if (simpleBlockResponse.status === 200) {
const code = simpleBlockResponse.data;
const description = extractBlockDescription(code);
const dependencies = extractDependencies(code);
const components = extractComponentUsage(code);
return {
name: blockName,
type: 'simple',
description: description || `Simple block: ${blockName}`,
code: code,
dependencies: dependencies,
componentsUsed: components,
size: code.length,
lines: code.split('\n').length,
usage: `Import and use directly in your application:\n\nimport ${blockName.charAt(0).toUpperCase() + blockName.slice(1).replace(/-/g, '')} from './blocks/${blockName}.svelte'`
};
}
}
catch (error) {
// Continue to check for complex block directory
}
// Check if it's a complex block directory
const directoryResponse = await githubApi.get(`/repos/${REPO_OWNER}/${REPO_NAME}/contents/${REGISTRY_BLOCKS_PATH}/${blockName}?ref=${REPO_BRANCH}`);
if (!directoryResponse.data) {
throw new Error(`Block "${blockName}" not found`);
}
const blockStructure = {
name: blockName,
type: 'complex',
description: `Complex block: ${blockName}`,
files: {},
structure: [],
totalFiles: 0,
dependencies: new Set(),
componentsUsed: new Set()
};
if (Array.isArray(directoryResponse.data)) {
blockStructure.totalFiles = directoryResponse.data.length;
for (const item of directoryResponse.data) {
if (item.type === 'file') {
const fileResponse = await githubRaw.get(`/${item.path}`);
const content = fileResponse.data;
const description = extractBlockDescription(content);
const dependencies = extractDependencies(content);
const components = extractComponentUsage(content);
blockStructure.files[item.name] = {
path: item.name,
content: content,
size: content.length,
lines: content.split('\n').length,
description: description,
dependencies: dependencies,
componentsUsed: components
};
dependencies.forEach((dep) => blockStructure.dependencies.add(dep));
components.forEach((comp) => blockStructure.componentsUsed.add(comp));
blockStructure.structure.push({
name: item.name,
type: 'file',
size: content.length,
description: description || `${item.name} - Block file`
});
if (description && blockStructure.description === `Complex block: ${blockName}`) {
blockStructure.description = description;
}
}
else if (item.type === 'dir' && includeComponents) {
// Handle subdirectories (like components)
const subDirResponse = await githubApi.get(`/repos/${REPO_OWNER}/${REPO_NAME}/contents/${item.path}?ref=${REPO_BRANCH}`);
if (Array.isArray(subDirResponse.data)) {
blockStructure.files[item.name] = {};
const subDirStructure = [];
for (const subItem of subDirResponse.data) {
if (subItem.type === 'file') {
const subFileResponse = await githubRaw.get(`/${subItem.path}`);
const content = subFileResponse.data;
const dependencies = extractDependencies(content);
const components = extractComponentUsage(content);
blockStructure.files[item.name][subItem.name] = {
path: `${item.name}/${subItem.name}`,
content: content,
size: content.length,
lines: content.split('\n').length,
dependencies: dependencies,
componentsUsed: components
};
dependencies.forEach((dep) => blockStructure.dependencies.add(dep));
components.forEach((comp) => blockStructure.componentsUsed.add(comp));
subDirStructure.push({
name: subItem.name,
type: 'file',
size: content.length
});
}
}
blockStructure.structure.push({
name: item.name,
type: 'directory',
files: subDirStructure,
count: subDirStructure.length
});
}
}
}
}
blockStructure.dependencies = Array.from(blockStructure.dependencies);
blockStructure.componentsUsed = Array.from(blockStructure.componentsUsed);
blockStructure.usage = generateComplexBlockUsage(blockName, blockStructure.structure);
return blockStructure;
}
catch (error) {
if (error.response?.status === 404) {
throw new Error(`Block "${blockName}" not found. Available blocks can be found in the shadcn-svelte blocks directory.`);
}
throw error;
}
}
/**
* Get all available blocks with categorization
* @param category Optional category filter
* @returns Promise with categorized block list
*/
async function getAvailableBlocks(category) {
try {
const response = await githubApi.get(`/repos/${REPO_OWNER}/${REPO_NAME}/contents/${REGISTRY_BLOCKS_PATH}?ref=${REPO_BRANCH}`);
if (!Array.isArray(response.data)) {
throw new Error('Unexpected response from GitHub API');
}
const blocks = {
calendar: [],
dashboard: [],
login: [],
sidebar: [],
authentication: [],
charts: [],
other: []
};
for (const item of response.data) {
const blockInfo = {
name: item.name.replace('.svelte', ''),
type: item.type === 'file' ? 'simple' : 'complex',
path: item.path,
size: item.size || 0,
lastModified: item.download_url ? 'Available' : 'Directory'
};
// Categorize blocks based on name patterns
if (item.name.includes('calendar')) {
blockInfo.description = 'Calendar component for date selection and scheduling';
blocks.calendar.push(blockInfo);
}
else if (item.name.includes('dashboard')) {
blockInfo.description = 'Dashboard layout with charts, metrics, and data display';
blocks.dashboard.push(blockInfo);
}
else if (item.name.includes('login') || item.name.includes('signin')) {
blockInfo.description = 'Authentication and login interface';
blocks.login.push(blockInfo);
}
else if (item.name.includes('sidebar')) {
blockInfo.description = 'Navigation sidebar component';
blocks.sidebar.push(blockInfo);
}
else if (item.name.includes('auth')) {
blockInfo.description = 'Authentication related components';
blocks.authentication.push(blockInfo);
}
else if (item.name.includes('chart') || item.name.includes('graph')) {
blockInfo.description = 'Data visualization and chart components';
blocks.charts.push(blockInfo);
}
else {
blockInfo.description = `${item.name} - Custom UI block`;
blocks.other.push(blockInfo);
}
}
// Sort blocks within each category
Object.keys(blocks).forEach(key => {
blocks[key].sort((a, b) => a.name.localeCompare(b.name));
});
// Filter by category if specified
if (category) {
const categoryLower = category.toLowerCase();
if (blocks[categoryLower]) {
return {
category,
blocks: blocks[categoryLower],
total: blocks[categoryLower].length,
description: `${category.charAt(0).toUpperCase() + category.slice(1)} blocks available in shadcn-svelte`,
usage: `Use 'getBlockCode' function with the block name to get the full source code and implementation details.`
};
}
else {
return {
category,
blocks: [],
total: 0,
availableCategories: Object.keys(blocks).filter(key => blocks[key].length > 0),
suggestion: `Category '${category}' not found. Available categories: ${Object.keys(blocks).filter(key => blocks[key].length > 0).join(', ')}`
};
}
}
const totalBlocks = Object.values(blocks).flat().length;
const nonEmptyCategories = Object.keys(blocks).filter(key => blocks[key].length > 0);
return {
categories: blocks,
totalBlocks,
availableCategories: nonEmptyCategories,
summary: Object.keys(blocks).reduce((acc, key) => {
if (blocks[key].length > 0) {
acc[key] = blocks[key].length;
}
return acc;
}, {}),
usage: "Use 'getBlockCode' function with a specific block name to get full source code and implementation details.",
examples: nonEmptyCategories.slice(0, 3).map(cat => blocks[cat][0] ? `${cat}: ${blocks[cat][0].name}` : '').filter(Boolean)
};
}
catch (error) {
if (error.response?.status === 404) {
throw new Error('Blocks directory not found in the shadcn-svelte registry');
}
throw error;
}
}
/**
* Build directory tree for shadcn-svelte repository
*/
async function buildDirectoryTree(owner = REPO_OWNER, repo = REPO_NAME, path = DOCS_BASE_PATH, branch = REPO_BRANCH) {
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;
if (!Array.isArray(contents)) {
if (contents.message) {
const message = contents.message;
if (message.includes('rate limit exceeded')) {
throw new Error(`GitHub API rate limit exceeded. ${message} Consider setting GITHUB_PERSONAL_ACCESS_TOKEN environment variable for higher rate limits.`);
}
else if (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: ${message}`);
}
}
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)}`);
}
}
const result = {
path,
type: 'directory',
children: {},
};
for (const item of contents) {
if (item.type === 'file') {
result.children[item.name] = {
path: item.path,
type: 'file',
name: item.name,
url: item.download_url,
sha: item.sha,
};
}
else if (item.type === 'dir') {
if (path.split('/').length < 8) {
try {
const subTree = await buildDirectoryTree(owner, repo, item.path, branch);
result.children[item.name] = subTree;
}
catch (error) {
logWarning(`Failed to fetch subdirectory ${item.path}: ${error instanceof Error ? error.message : String(error)}`);
result.children[item.name] = {
path: item.path,
type: 'directory',
error: 'Failed to fetch contents'
};
}
}
}
}
return result;
}
catch (error) {
logError(`Error building directory tree for ${path}`, error);
if (error.message && (error.message.includes('rate limit') || error.message.includes('GitHub API error'))) {
throw error;
}
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 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 if provided.`);
}
else {
throw new Error(`GitHub API error (${status}): ${message}`);
}
}
throw error;
}
}
/**
* Extract dependencies from import statements (updated for Svelte)
* @param code The source code to analyze
* @returns Array of dependency names
*/
function extractDependencies(code) {
const dependencies = [];
const importRegex = /import\s+.*?\s+from\s+['"]([@\w\/\-\.]+)['"]/g;
let match;
while ((match = importRegex.exec(code)) !== null) {
const dep = match[1];
if (!dep.startsWith('./') && !dep.startsWith('../') && !dep.startsWith('$') && !dep.startsWith('/')) {
dependencies.push(dep);
}
}
return [...new Set(dependencies)];
}
/**
* Extract imports from import statements (Svelte specific)
* @param code The source code to analyze
* @returns Array of imported items
*/
function extractImports(code) {
const imports = [];
const patterns = [
/import\s+\{([^}]+)\}\s+from/g, // Named imports
/import\s+(\w+)\s+from/g, // Default imports
/import\s+\*\s+as\s+(\w+)\s+from/g // Namespace imports
];
patterns.forEach(pattern => {
let match;
while ((match = pattern.exec(code)) !== null) {
if (match[1].includes(',')) {
const namedImports = match[1].split(',').map(imp => imp.trim());
imports.push(...namedImports);
}
else {
imports.push(match[1].trim());
}
}
});
return [...new Set(imports)];
}
/**
* Extract description from block code comments
* @param code The source code to analyze
* @returns Extracted description or null
*/
function extractBlockDescription(code) {
const descriptionRegex = /\/\*\*[\s\S]*?\*\/|\/\/\s*(.+)|<!--\s*(.+?)\s*-->/;
const match = code.match(descriptionRegex);
if (match) {
const description = match[0]
.replace(/\/\*\*|\*\/|\*|\/\/|<!--|-->/g, '')
.trim()
.split('\n')[0]
.trim();
return description.length > 0 ? description : null;
}
return null;
}
/**
* Extract component usage from code
* @param code The source code to analyze
* @returns Array of component names used
*/
function extractComponentUsage(code) {
const components = [];
const importRegex = /import\s+\{([^}]+)\}\s+from/g;
let match;
while ((match = importRegex.exec(code)) !== null) {
const imports = match[1].split(',').map(imp => imp.trim());
imports.forEach(imp => {
if (imp[0] && imp[0] === imp[0].toUpperCase()) {
components.push(imp);
}
});
}
// Look for Svelte component usage
const svelteComponentRegex = /<([A-Z][a-zA-Z0-9]*)/g;
while ((match = svelteComponentRegex.exec(code)) !== null) {
components.push(match[1]);
}
return [...new Set(components)];
}
/**
* Generate usage instructions for complex blocks
* @param blockName Name of the block
* @param structure Structure information
* @returns Usage instructions string
*/
function generateComplexBlockUsage(blockName, structure) {
const hasComponents = structure.some(item => item.name === 'components');
let usage = `To use the ${blockName} block:\n\n`;
usage += `1. Copy the main files to your project:\n`;
structure.forEach(item => {
if (item.type === 'file') {
usage += ` - ${item.name}\n`;
}
else if (item.type === 'directory' && item.name === 'components') {
usage += ` - components/ directory (${item.count} files)\n`;
}
});
if (hasComponents) {
usage += `\n2. Copy the components to your components directory\n`;
usage += `3. Update import paths as needed\n`;
usage += `4. Ensure all dependencies are installed\n`;
}
else {
usage += `\n2. Update import paths as needed\n`;
usage += `3. Ensure all dependencies are installed\n`;
}
return usage;
}
/**
* Enhanced buildDirectoryTree with fallback for rate limits
*/
async function buildDirectoryTreeWithFallback(owner = REPO_OWNER, repo = REPO_NAME, path = DOCS_BASE_PATH, branch = REPO_BRANCH) {
try {
return await buildDirectoryTree(owner, repo, path, branch);
}
catch (error) {
if (error.message && error.message.includes('rate limit') && path === DOCS_BASE_PATH) {
logWarning('Using fallback directory structure due to rate limit');
return getBasicSvelteStructure();
}
throw error;
}
}
/**
* Provides a basic directory structure for shadcn-svelte registry
*/
function getBasicSvelteStructure() {
return {
path: DOCS_BASE_PATH,
type: 'directory',
note: 'Basic structure provided due to API limitations',
children: {
'ui': {
path: `${DOCS_BASE_PATH}/ui`,
type: 'directory',
description: 'Contains all Svelte UI components',
note: 'Component directories with .svelte files are located here'
},
'examples': {
path: `${DOCS_BASE_PATH}/examples`,
type: 'directory',
description: 'Contains component demo examples',
note: 'Demo files showing component usage'
},
'blocks': {
path: `${DOCS_BASE_PATH}/blocks`,
type: 'directory',
description: 'Contains pre-built blocks and layouts',
note: 'Complex components and page layouts'
},
'hooks': {
path: `${DOCS_BASE_PATH}/hooks`,
type: 'directory',
description: 'Contains custom Svelte hooks',
note: 'Reusable Svelte hooks and utilities'
},
'lib': {
path: `${DOCS_BASE_PATH}/lib`,
type: 'directory',
description: 'Contains utility libraries and functions',
note: 'Helper functions and utilities'
}
}
};
}
/**
* Set or update GitHub API key for higher rate limits
* @param apiKey GitHub Personal Access Token
*/
function setGitHubApiKey(apiKey) {
if (apiKey && apiKey.trim()) {
githubApi.defaults.headers['Authorization'] = `Bearer ${apiKey.trim()}`;
logInfo('GitHub API key updated successfully');
}
else {
delete githubApi.defaults.headers['Authorization'];
logWarning('GitHub API key removed - using unauthenticated requests');
}
}
/**
* Get current GitHub API rate limit status
* @returns Promise with rate limit information
*/
async function getGitHubRateLimit() {
try {
const response = await githubApi.get('/rate_limit');
return response.data;
}
catch (error) {
throw new Error(`Failed to get rate limit info: ${error.message}`);
}
}
export const axios = {
githubRaw,
githubApi,
buildDirectoryTree: buildDirectoryTreeWithFallback,
buildDirectoryTreeWithFallback,
getComponentSource,
getComponentDemo,
getAvailableComponents,
getComponentMetadata,
getBlockCode,
getAvailableBlocks,
setGitHubApiKey,
getGitHubRateLimit,
// Path constants for shadcn-svelte
paths: {
REPO_OWNER,
REPO_NAME,
REPO_BRANCH,
DOCS_BASE_PATH,
REGISTRY_UI_PATH,
REGISTRY_EXAMPLES_PATH,
REGISTRY_BLOCKS_PATH,
REGISTRY_HOOKS_PATH,
REGISTRY_LIB_PATH
}
};