#!/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);