Skip to main content
Glama

MCP Modus

index.ts20.9 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, Tool, } from "@modelcontextprotocol/sdk/types.js"; import { readFileSync, readdirSync, existsSync } from "fs"; import { join, dirname } from "path"; import { fileURLToPath } from "url"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); interface ComponentDoc { filename: string; component: string; content: string; } interface RuleDoc { filename: string; category: string; content: string; } interface SetupDoc { filename: string; setupType: string; content: string; } class ModusDocsServer { private server: Server; private docs: ComponentDoc[] = []; private rules: RuleDoc[] = []; private setup: SetupDoc[] = []; private docsPath: string; private rulesPath: string; private setupPath: string; constructor() { this.server = new Server( { name: "mcp-modus", version: "1.0.0", }, { capabilities: { tools: {}, }, } ); // Try to find docs directory - check both dev and production paths const possibleDocsPaths = [ join(__dirname, "..", "docs"), // Development: dist/../docs join(__dirname, "docs"), // Production: dist/docs join(process.cwd(), "docs"), // Current working directory ]; const possibleRulesPaths = [ join(__dirname, "..", "rules"), // Development: dist/../rules join(__dirname, "rules"), // Production: dist/rules join(process.cwd(), "rules"), // Current working directory ]; const possibleSetupPaths = [ join(__dirname, "..", "setup"), // Development: dist/../setup join(__dirname, "setup"), // Production: dist/setup join(process.cwd(), "setup"), // Current working directory ]; this.docsPath = possibleDocsPaths.find((p) => existsSync(p)) || possibleDocsPaths[0]; this.rulesPath = possibleRulesPaths.find((p) => existsSync(p)) || possibleRulesPaths[0]; this.setupPath = possibleSetupPaths.find((p) => existsSync(p)) || possibleSetupPaths[0]; this.setupHandlers(); this.loadDocs(); this.loadRules(); this.loadSetup(); } private loadDocs(): void { if (!existsSync(this.docsPath)) { console.error(`Documentation directory not found at: ${this.docsPath}`); console.error("Please run: node download-docs.js"); return; } const files = readdirSync(this.docsPath).filter((f) => f.endsWith(".md")); for (const file of files) { const content = readFileSync(join(this.docsPath, file), "utf-8"); const component = file.replace("modus-wc-", "").replace(".md", ""); this.docs.push({ filename: file, component, content, }); } console.error(`Loaded ${this.docs.length} component documentation files`); } private loadRules(): void { if (!existsSync(this.rulesPath)) { console.error(`Rules directory not found at: ${this.rulesPath}`); console.error("Please run: node download-docs.js"); return; } const files = readdirSync(this.rulesPath).filter((f) => f.endsWith(".md")); for (const file of files) { const content = readFileSync(join(this.rulesPath, file), "utf-8"); const category = file.replace(".md", "").replace("modus_", ""); this.rules.push({ filename: file, category, content, }); } console.error(`Loaded ${this.rules.length} design rules files`); } private loadSetup(): void { if (!existsSync(this.setupPath)) { console.error(`Setup directory not found at: ${this.setupPath}`); console.error("Please run: node download-docs.js"); return; } const files = readdirSync(this.setupPath).filter((f) => f.endsWith(".md")); for (const file of files) { const content = readFileSync(join(this.setupPath, file), "utf-8"); let setupType = file.replace(".md", "").replace("setup_", ""); // Map filenames to more user-friendly types if (setupType === "universal_rules") setupType = "universal"; if (setupType === "theme_usage") setupType = "theme"; this.setup.push({ filename: file, setupType, content, }); } console.error(`Loaded ${this.setup.length} setup guide files`); } private setupHandlers(): void { this.server.setRequestHandler(ListToolsRequestSchema, async () => { const tools: Tool[] = [ { name: "search_components", description: "Search for Modus Web Components by name or keyword. Returns a list of matching components with brief descriptions.", inputSchema: { type: "object", properties: { query: { type: "string", description: "Search query (component name, keyword, or feature)", }, }, required: ["query"], }, }, { name: "get_component_docs", description: "Get the complete documentation for a specific Modus Web Component including attributes, events, and usage examples.", inputSchema: { type: "object", properties: { component: { type: "string", description: 'The component name (e.g., "button", "card", "modal")', }, }, required: ["component"], }, }, { name: "list_all_components", description: "List all available Modus Web Components with their categories.", inputSchema: { type: "object", properties: {}, }, }, { name: "find_by_attribute", description: "Find components that have a specific attribute or property.", inputSchema: { type: "object", properties: { attribute: { type: "string", description: 'The attribute name to search for (e.g., "disabled", "color", "size")', }, }, required: ["attribute"], }, }, { name: "get_design_rules", description: "Get specific design rules for Modus Web Components (colors, icons, spacing, typography, etc.).", inputSchema: { type: "object", properties: { category: { type: "string", description: 'The design rule category (e.g., "colors", "icons", "spacing", "typography", "breakpoints", "radius_stroke")', }, }, required: ["category"], }, }, { name: "search_design_rules", description: "Search across all design rules by keyword or term.", inputSchema: { type: "object", properties: { query: { type: "string", description: 'Search query for design rules (e.g., "primary color", "icon size", "spacing scale")', }, }, required: ["query"], }, }, { name: "list_design_categories", description: "List all available design rule categories.", inputSchema: { type: "object", properties: {}, }, }, { name: "get_setup_guide", description: "Get setup instructions for HTML or React projects using Modus Web Components.", inputSchema: { type: "object", properties: { type: { type: "string", description: 'The setup type ("html", "react", "testing")', }, }, required: ["type"], }, }, { name: "get_theme_usage", description: "Get theme implementation guidelines and usage instructions.", inputSchema: { type: "object", properties: {}, }, }, { name: "get_development_rules", description: "Get universal development rules and best practices for Modus Web Components.", inputSchema: { type: "object", properties: {}, }, }, ]; return { tools }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case "search_components": return await this.searchComponents((args?.query as string) || ""); case "get_component_docs": return await this.getComponentDocs( (args?.component as string) || "" ); case "list_all_components": return await this.listAllComponents(); case "find_by_attribute": return await this.findByAttribute( (args?.attribute as string) || "" ); case "get_design_rules": return await this.getDesignRules((args?.category as string) || ""); case "search_design_rules": return await this.searchDesignRules((args?.query as string) || ""); case "list_design_categories": return await this.listDesignCategories(); case "get_setup_guide": return await this.getSetupGuide((args?.type as string) || ""); case "get_theme_usage": return await this.getThemeUsage(); case "get_development_rules": return await this.getDevelopmentRules(); default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [{ type: "text", text: `Error: ${errorMessage}` }], isError: true, }; } }); } private async searchComponents(query: string): Promise<any> { const normalizedQuery = query.toLowerCase(); const results: Array<{ component: string; filename: string; relevance: string; }> = []; for (const doc of this.docs) { const content = doc.content.toLowerCase(); const componentName = doc.component.toLowerCase(); if ( componentName.includes(normalizedQuery) || content.includes(normalizedQuery) ) { // Extract the first paragraph or description const lines = doc.content.split("\n"); let description = ""; for (const line of lines) { if ( line.trim() && !line.startsWith("#") && !line.startsWith("Tag:") ) { description = line.trim(); break; } } results.push({ component: doc.component, filename: doc.filename, relevance: description || "Modus Web Component", }); } } if (results.length === 0) { return { content: [ { type: "text", text: `No components found matching "${query}". Try searching for common UI elements like "button", "input", "modal", "card", etc.`, }, ], }; } const resultText = results .map((r) => `**${r.component}**\n${r.relevance}\n`) .join("\n"); return { content: [ { type: "text", text: `Found ${results.length} component(s) matching "${query}":\n\n${resultText}`, }, ], }; } private async getComponentDocs(component: string): Promise<any> { const normalizedComponent = component .toLowerCase() .replace("modus-wc-", ""); const doc = this.docs.find( (d) => d.component.toLowerCase() === normalizedComponent ); if (!doc) { const availableComponents = this.docs.map((d) => d.component).join(", "); return { content: [ { type: "text", text: `Component "${component}" not found.\n\nAvailable components: ${availableComponents}`, }, ], }; } return { content: [ { type: "text", text: doc.content, }, ], }; } private async listAllComponents(): Promise<any> { const componentsByCategory: Record<string, string[]> = {}; for (const doc of this.docs) { // Extract category from content const categoryMatch = doc.content.match(/Category:\s*([^\n]+)/i); const category = categoryMatch ? categoryMatch[1].trim() : "Other"; if (!componentsByCategory[category]) { componentsByCategory[category] = []; } componentsByCategory[category].push(doc.component); } let resultText = `# Modus Web Components (${this.docs.length} components)\n\n`; for (const [category, components] of Object.entries( componentsByCategory ).sort()) { resultText += `## ${category}\n`; resultText += components .sort() .map((c) => `- ${c}`) .join("\n"); resultText += "\n\n"; } return { content: [ { type: "text", text: resultText, }, ], }; } private async findByAttribute(attribute: string): Promise<any> { const normalizedAttribute = attribute.toLowerCase(); const results: Array<{ component: string; context: string }> = []; for (const doc of this.docs) { const lines = doc.content.split("\n"); for (let i = 0; i < lines.length; i++) { const line = lines[i]; const lowerLine = line.toLowerCase(); // Look for attribute definitions like: - **`disabled`** if ( lowerLine.includes(`\`${normalizedAttribute}\``) && (line.startsWith("-") || line.startsWith("•")) ) { // Get context - the attribute definition and its properties const contextStart = i; let contextEnd = i + 1; // Include lines until we hit another attribute or empty line while ( contextEnd < lines.length && !lines[contextEnd].match(/^-\s+\*\*`/) && contextEnd < i + 10 ) { if (lines[contextEnd].trim() === "") { break; } contextEnd++; } const context = lines.slice(contextStart, contextEnd).join("\n"); results.push({ component: doc.component, context: context.trim(), }); break; } } } if (results.length === 0) { return { content: [ { type: "text", text: `No components found with attribute "${attribute}".`, }, ], }; } const resultText = results .map((r) => `**${r.component}**\n\`\`\`\n${r.context}\n\`\`\`\n`) .join("\n"); return { content: [ { type: "text", text: `Found ${results.length} component(s) with attribute "${attribute}":\n\n${resultText}`, }, ], }; } private async getDesignRules(category: string): Promise<any> { const normalizedCategory = category.toLowerCase(); const rule = this.rules.find( (r) => r.category.toLowerCase() === normalizedCategory || r.filename.toLowerCase().includes(normalizedCategory) ); if (!rule) { const availableCategories = this.rules.map((r) => r.category).join(", "); return { content: [ { type: "text", text: `Design rule category "${category}" not found.\n\nAvailable categories: ${availableCategories}`, }, ], }; } return { content: [ { type: "text", text: rule.content, }, ], }; } private async searchDesignRules(query: string): Promise<any> { const normalizedQuery = query.toLowerCase(); const results: Array<{ category: string; filename: string; relevance: string; }> = []; for (const rule of this.rules) { const content = rule.content.toLowerCase(); const categoryName = rule.category.toLowerCase(); if ( categoryName.includes(normalizedQuery) || content.includes(normalizedQuery) ) { // Extract the first meaningful line as description const lines = rule.content.split("\n"); let description = ""; for (const line of lines) { if (line.trim() && !line.startsWith("#") && !line.startsWith("---")) { description = line.trim(); break; } } results.push({ category: rule.category, filename: rule.filename, relevance: description || "Design rule documentation", }); } } if (results.length === 0) { return { content: [ { type: "text", text: `No design rules found matching "${query}". Try searching for terms like "color", "icon", "spacing", "typography", etc.`, }, ], }; } const resultText = results .map((r) => `**${r.category}**\n${r.relevance}\n`) .join("\n"); return { content: [ { type: "text", text: `Found ${results.length} design rule(s) matching "${query}":\n\n${resultText}`, }, ], }; } private async listDesignCategories(): Promise<any> { let resultText = `# Modus Design Rules (${this.rules.length} categories)\n\n`; for (const rule of this.rules.sort((a, b) => a.category.localeCompare(b.category) )) { // Extract brief description from content const lines = rule.content.split("\n"); let description = "Design guidelines and specifications"; for (const line of lines) { if ( line.trim() && !line.startsWith("#") && !line.startsWith("---") && line.length > 10 ) { description = line.trim().substring(0, 100) + (line.length > 100 ? "..." : ""); break; } } resultText += `## ${rule.category}\n${description}\n\n`; } return { content: [ { type: "text", text: resultText, }, ], }; } private async getSetupGuide(type: string): Promise<any> { const normalizedType = type.toLowerCase(); const guide = this.setup.find( (s) => s.setupType.toLowerCase() === normalizedType || s.filename.toLowerCase().includes(normalizedType) ); if (!guide) { const availableTypes = this.setup.map((s) => s.setupType).join(", "); return { content: [ { type: "text", text: `Setup guide type "${type}" not found.\n\nAvailable types: ${availableTypes}`, }, ], }; } return { content: [ { type: "text", text: guide.content, }, ], }; } private async getThemeUsage(): Promise<any> { const themeGuide = this.setup.find( (s) => s.setupType === "theme" || s.filename.includes("theme") ); if (!themeGuide) { return { content: [ { type: "text", text: "Theme usage guide not found. Please run: node download-docs.js", }, ], }; } return { content: [ { type: "text", text: themeGuide.content, }, ], }; } private async getDevelopmentRules(): Promise<any> { const universalGuide = this.setup.find( (s) => s.setupType === "universal" || s.filename.includes("universal") ); if (!universalGuide) { return { content: [ { type: "text", text: "Universal development rules not found. Please run: node download-docs.js", }, ], }; } return { content: [ { type: "text", text: universalGuide.content, }, ], }; } async run(): Promise<void> { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error("Modus Web Components MCP Server running on stdio"); } } const server = new ModusDocsServer(); server.run().catch(console.error);

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/julianoczkowski/mcp-modus'

If you have feedback or need assistance with the MCP directory API, please join our Discord server