├── .gitignore ├── tsconfig.json ├── LICENSE ├── package.json ├── README.md └── src └── index.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | *.log 4 | .env* -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules"] 15 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 fluxui-dev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fluxui-mcp-server", 3 | "version": "0.1.0", 4 | "description": "MCP server for Flux UI component references", 5 | "type": "module", 6 | "license": "MIT", 7 | "bin": { 8 | "fluxui-server": "build/index.js" 9 | }, 10 | "files": [ 11 | "build" 12 | ], 13 | "scripts": { 14 | "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"", 15 | "prepare": "npm run build", 16 | "watch": "tsc --watch", 17 | "inspector": "npx @modelcontextprotocol/inspector build/index.js" 18 | }, 19 | "dependencies": { 20 | "@modelcontextprotocol/sdk": "^0.6.0", 21 | "axios": "^1.7.9", 22 | "cheerio": "^1.0.0-rc.12" 23 | }, 24 | "devDependencies": { 25 | "@types/node": "^20.11.24", 26 | "typescript": "^5.3.3" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "git+https://github.com/your-github/fluxui-mcp-server.git" 31 | }, 32 | "author": "Your Name ", 33 | "bugs": { 34 | "url": "https://github.com/your-github/fluxui-mcp-server/issues" 35 | }, 36 | "homepage": "https://github.com/your-github/fluxui-mcp-server#readme", 37 | "keywords": [ 38 | "fluxui", 39 | "MCP", 40 | "model context protocol", 41 | "component references" 42 | ] 43 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flux UI MCP Server 2 | 3 | MCP server for Flux UI component references 4 | 5 | This is a TypeScript-based MCP server that provides reference information for Flux UI components. It implements a Model Context Protocol (MCP) server that helps AI assistants access Flux UI component documentation and examples. 6 | 7 | ## Features 8 | 9 | ### Tools 10 | 11 | - `list_flux_components` - Get a list of all available Flux UI components 12 | - `get_flux_component_details` - Get detailed information about a specific component 13 | - `get_flux_component_examples` - Get usage examples for a specific component 14 | - `search_flux_components` - Search for components by keyword 15 | 16 | ### Functionality 17 | 18 | This server scrapes and caches information from: 19 | - The official Flux UI documentation site (https://fluxui.dev) 20 | 21 | It provides structured data including: 22 | - Component descriptions 23 | - Usage examples 24 | - Props 25 | - Code samples 26 | 27 | ## Development 28 | 29 | Install dependencies: 30 | ```bash 31 | npm install 32 | ``` 33 | 34 | Build the server: 35 | ```bash 36 | npm run build 37 | ``` 38 | 39 | For development with auto-rebuild: 40 | ```bash 41 | npm run watch 42 | ``` 43 | 44 | ## Installation 45 | 46 | ### Claude Desktop Configuration 47 | 48 | To use with Claude Desktop, add the server config: 49 | 50 | On MacOS: `~/Library/Application Support/Claude/claude_desktop_config.json` 51 | On Windows: `%APPDATA%/Claude/claude_desktop_config.json` 52 | 53 | #### Option 1: Using local build 54 | 55 | ```json 56 | { 57 | "mcpServers": { 58 | "fluxui-server": { 59 | "command": "/path/to/fluxui-mcp-server/build/index.js" 60 | } 61 | } 62 | } 63 | ``` 64 | 65 | #### Option 2: Using npx command 66 | 67 | ```json 68 | { 69 | "mcpServers": { 70 | "fluxui-server": { 71 | "command": "npx", 72 | "args": ["-y", "fluxui-mcp-server"] 73 | } 74 | } 75 | } 76 | ``` 77 | 78 | ### Windsurf Configuration 79 | 80 | Add this to your `./codeium/windsurf/model_config.json`: 81 | 82 | ```json 83 | { 84 | "mcpServers": { 85 | "fluxui-server": { 86 | "command": "npx", 87 | "args": ["-y", "fluxui-mcp-server"] 88 | } 89 | } 90 | } 91 | ``` 92 | 93 | ### Cursor Configuration 94 | 95 | Add this to your `.cursor/mcp.json`: 96 | 97 | ```json 98 | { 99 | "mcpServers": { 100 | "fluxui-server": { 101 | "command": "npx", 102 | "args": ["-y", "fluxui-mcp-server"] 103 | } 104 | } 105 | } 106 | ``` 107 | 108 | ### Debugging 109 | 110 | Since MCP servers communicate over stdio, debugging can be challenging. We recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector), which is available as a package script: 111 | 112 | ```bash 113 | npm run inspector 114 | ``` 115 | 116 | The Inspector will provide a URL to access debugging tools in your browser. -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * MCP server for Flux UI component references 5 | * This server provides tools to: 6 | * - List all available Flux UI components 7 | * - Get detailed information about specific components 8 | * - Get usage examples for components 9 | * - Search for components by keyword 10 | */ 11 | 12 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 13 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 14 | import { 15 | CallToolRequestSchema, 16 | ErrorCode, 17 | ListToolsRequestSchema, 18 | McpError, 19 | } from "@modelcontextprotocol/sdk/types.js"; 20 | import axios from "axios"; 21 | import * as cheerio from "cheerio"; 22 | import { Element } from 'domhandler'; 23 | 24 | /** 25 | * Interface for component information 26 | */ 27 | interface ComponentInfo { 28 | name: string; 29 | description: string; 30 | url: string; 31 | importStatement?: string; 32 | props?: Record; // Component name -> Props list 33 | examples?: ComponentExample[]; 34 | } 35 | 36 | /** 37 | * Interface for component property information 38 | */ 39 | interface ComponentProp { 40 | name: string; // Renamed from 'prop' for clarity if needed elsewhere 41 | type: string; 42 | default?: string; 43 | description?: string; // Flux UI seems to sometimes have descriptions in API tables 44 | required?: boolean; // May need to infer this or look for specific markers 45 | } 46 | 47 | /** 48 | * Interface for component example 49 | */ 50 | interface ComponentExample { 51 | title: string; 52 | code: string; 53 | description?: string; 54 | } 55 | 56 | /** 57 | * FluxUiServer class that handles all the component reference functionality 58 | */ 59 | class FluxUiServer { 60 | private server: Server; 61 | private axiosInstance; 62 | private componentCache: Map = new Map(); 63 | private componentsListCache: ComponentInfo[] | null = null; 64 | private readonly FLUX_DOCS_URL = "https://fluxui.dev"; 65 | 66 | constructor() { 67 | this.server = new Server( 68 | { 69 | name: "fluxui-server", 70 | version: "0.1.0", 71 | }, 72 | { 73 | capabilities: { 74 | tools: {}, 75 | }, 76 | } 77 | ); 78 | 79 | this.axiosInstance = axios.create({ 80 | timeout: 15000, // Increased timeout slightly 81 | headers: { 82 | "User-Agent": "Mozilla/5.0 (compatible; FluxUiMcpServer/0.1.0)", 83 | }, 84 | }); 85 | 86 | this.setupToolHandlers(); 87 | 88 | this.server.onerror = (error) => console.error("[MCP Error]", error); 89 | process.on("SIGINT", async () => { 90 | await this.server.close(); 91 | process.exit(0); 92 | }); 93 | } 94 | 95 | /** 96 | * Set up the tool handlers for the server 97 | */ 98 | private setupToolHandlers() { 99 | this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ 100 | tools: [ 101 | { 102 | name: "list_flux_components", 103 | description: "Get a list of all available Flux UI components", 104 | inputSchema: { 105 | type: "object", 106 | properties: {}, 107 | required: [], 108 | }, 109 | }, 110 | { 111 | name: "get_flux_component_details", 112 | description: "Get detailed information about a specific Flux UI component", 113 | inputSchema: { 114 | type: "object", 115 | properties: { 116 | componentName: { 117 | type: "string", 118 | description: 'Name of the Flux UI component (e.g., "accordion", "button")', 119 | }, 120 | }, 121 | required: ["componentName"], 122 | }, 123 | }, 124 | { 125 | name: "get_flux_component_examples", 126 | description: "Get usage examples for a specific Flux UI component", 127 | inputSchema: { 128 | type: "object", 129 | properties: { 130 | componentName: { 131 | type: "string", 132 | description: 'Name of the Flux UI component (e.g., "accordion", "button")', 133 | }, 134 | }, 135 | required: ["componentName"], 136 | }, 137 | }, 138 | { 139 | name: "search_flux_components", 140 | description: "Search for Flux UI components by keyword", 141 | inputSchema: { 142 | type: "object", 143 | properties: { 144 | query: { 145 | type: "string", 146 | description: "Search query to find relevant components", 147 | }, 148 | }, 149 | required: ["query"], 150 | }, 151 | }, 152 | ], 153 | })); 154 | 155 | this.server.setRequestHandler(CallToolRequestSchema, async (request) => { 156 | switch (request.params.name) { 157 | case "list_flux_components": 158 | return await this.handleListComponents(); 159 | case "get_flux_component_details": 160 | return await this.handleGetComponentDetails(request.params.arguments); 161 | case "get_flux_component_examples": 162 | return await this.handleGetComponentExamples(request.params.arguments); 163 | case "search_flux_components": 164 | return await this.handleSearchComponents(request.params.arguments); 165 | default: 166 | throw new McpError( 167 | ErrorCode.MethodNotFound, 168 | `Unknown tool: ${request.params.name}` 169 | ); 170 | } 171 | }); 172 | } 173 | 174 | /** 175 | * Handle the list_flux_components tool request 176 | */ 177 | private async handleListComponents() { 178 | try { 179 | if (!this.componentsListCache) { 180 | // Fetch the main components page or sidebar structure 181 | // This needs inspection of fluxui.dev to find the component list reliably 182 | // Let's assume we fetch the base URL and look for links starting with /components/ 183 | const response = await this.axiosInstance.get(`${this.FLUX_DOCS_URL}/components`); 184 | const $ = cheerio.load(response.data); 185 | 186 | const components: ComponentInfo[] = []; 187 | const componentUrls = new Set(); // Avoid duplicates 188 | 189 | // Look for links within the navigation or main content area 190 | // Adjust selector based on actual site structure 191 | $('a[href^="/components/"]').each((_, element) => { 192 | const link = $(element); 193 | const url = link.attr("href"); 194 | 195 | if (url && url !== "/components" && !componentUrls.has(url)) { 196 | // Basic check to avoid the parent page 197 | // Extract name from URL 198 | const parts = url.split("/").filter(part => part); // filter removes empty strings 199 | const name = parts[parts.length - 1]; 200 | 201 | if (name && !name.includes('#')) { // Basic check for valid component name 202 | componentUrls.add(url); 203 | components.push({ 204 | name, 205 | description: "", // Will be populated when fetching details 206 | url: `${this.FLUX_DOCS_URL}${url}`, 207 | }); 208 | } 209 | } 210 | }); 211 | 212 | // Sort components alphabetically by name 213 | components.sort((a, b) => a.name.localeCompare(b.name)); 214 | 215 | this.componentsListCache = components; 216 | } 217 | 218 | return this.createSuccessResponse( 219 | this.componentsListCache.map(c => ({ name: c.name, url: c.url })) // Return only name and URL for list 220 | ); 221 | 222 | } catch (error) { 223 | this.handleAxiosError(error, "Failed to fetch Flux UI components list"); 224 | } 225 | } 226 | 227 | /** 228 | * Validates component name from arguments 229 | */ 230 | private validateComponentName(args: any): string { 231 | if (!args?.componentName || typeof args.componentName !== "string") { 232 | throw new McpError( 233 | ErrorCode.InvalidParams, 234 | "Component name is required and must be a string" 235 | ); 236 | } 237 | // Normalize component name if needed (e.g., lowercase) 238 | return args.componentName.toLowerCase(); 239 | } 240 | 241 | /** 242 | * Validates search query from arguments 243 | */ 244 | private validateSearchQuery(args: any): string { 245 | if (!args?.query || typeof args.query !== "string") { 246 | throw new McpError( 247 | ErrorCode.InvalidParams, 248 | "Search query is required and must be a string" 249 | ); 250 | } 251 | return args.query.toLowerCase(); 252 | } 253 | 254 | /** 255 | * Handles Axios errors consistently 256 | */ 257 | private handleAxiosError(error: unknown, context: string): never { 258 | if (axios.isAxiosError(error)) { 259 | console.error(`Axios error during "${context}": ${error.message}`, error.response?.status, error.config?.url); 260 | if (error.response?.status === 404) { 261 | throw new McpError( 262 | ErrorCode.InvalidParams, // Use InvalidParams for 404 instead of NotFound 263 | `${context} - Resource not found (404)` 264 | ); 265 | } else { 266 | const status = error.response?.status || 'N/A'; 267 | const message = error.message; 268 | throw new McpError( 269 | ErrorCode.InternalError, 270 | `Failed during "${context}" operation. Status: ${status}. Error: ${message}` 271 | ); 272 | } 273 | } 274 | console.error(`Non-Axios error during "${context}":`, error); 275 | // Re-throw non-Axios errors or wrap them if needed 276 | throw error instanceof McpError ? error : new McpError(ErrorCode.InternalError, `An unexpected error occurred during "${context}".`); 277 | } 278 | 279 | 280 | /** 281 | * Creates a standardized success response 282 | */ 283 | private createSuccessResponse(data: any) { 284 | return { 285 | content: [ 286 | { 287 | type: "text", 288 | // Attempt to stringify, handle potential circular references safely 289 | text: JSON.stringify(data, (key, value) => { 290 | if (typeof value === 'object' && value !== null) { 291 | // Basic circular reference check placeholder - might need a more robust solution 292 | // if complex objects are returned that Cheerio might create. 293 | // For simple data structures, this might be okay. 294 | } 295 | return value; 296 | }, 2) 297 | }, 298 | ], 299 | }; 300 | } 301 | 302 | 303 | /** 304 | * Handle the get_flux_component_details tool request 305 | */ 306 | private async handleGetComponentDetails(args: any) { 307 | const componentName = this.validateComponentName(args); 308 | 309 | try { 310 | // Check cache first 311 | if (this.componentCache.has(componentName)) { 312 | const cachedData = this.componentCache.get(componentName); 313 | console.error(`Cache hit for ${componentName}`); 314 | return this.createSuccessResponse(cachedData); 315 | } 316 | console.error(`Cache miss for ${componentName}, fetching...`); 317 | 318 | // Fetch component details 319 | const componentInfo = await this.fetchComponentDetails(componentName); 320 | 321 | // Save to cache 322 | this.componentCache.set(componentName, componentInfo); 323 | console.error(`Cached details for ${componentName}`); 324 | 325 | return this.createSuccessResponse(componentInfo); 326 | } catch (error) { 327 | console.error(`Error fetching details for ${componentName}:`, error); 328 | // Ensure handleAxiosError is called correctly or rethrow McpError 329 | if (error instanceof McpError) { 330 | throw error; 331 | } 332 | this.handleAxiosError(error, `fetching details for component "${componentName}"`); 333 | } 334 | } 335 | 336 | /** 337 | * Fetches component details from the Flux UI documentation 338 | */ 339 | private async fetchComponentDetails(componentName: string): Promise { 340 | const componentUrl = `${this.FLUX_DOCS_URL}/components/${componentName}`; 341 | console.error(`Fetching URL: ${componentUrl}`); 342 | const response = await this.axiosInstance.get(componentUrl); 343 | const $ = cheerio.load(response.data); 344 | console.error(`Successfully loaded HTML for ${componentName}`); 345 | 346 | // Extract component information 347 | const title = $("h1").first().text().trim(); 348 | const description = this.extractDescription($); 349 | const examples = this.extractExamples($); // Extract examples first to find import statement 350 | const importStatement = this.findImportStatement(examples); 351 | const props = this.extractProps($); 352 | 353 | console.error(`Extracted for ${componentName}: Title=${title}, Desc=${description.substring(0,50)}..., Import=${importStatement}, Props=${Object.keys(props).length}, Examples=${examples.length}`); 354 | 355 | return { 356 | name: title || componentName, // Use extracted title if available 357 | description, 358 | url: componentUrl, 359 | importStatement, 360 | props: Object.keys(props).length > 0 ? props : undefined, 361 | examples, // Include examples in details as well 362 | }; 363 | } 364 | 365 | /** 366 | * Extracts component description from the page 367 | */ 368 | private extractDescription($: cheerio.CheerioAPI): string { 369 | // Find the first

tag that is a sibling of the first

370 | const descriptionElement = $("h1").first().next("p"); 371 | return descriptionElement.text().trim(); 372 | } 373 | 374 | /** 375 | * Extracts usage examples and code snippets from the page 376 | */ 377 | private extractExamples($: cheerio.CheerioAPI): ComponentExample[] { 378 | const examples: ComponentExample[] = []; 379 | // Look for sections containing code examples. Flux UI seems to use blocks 380 | // with a 'Code' tab or similar structure. 381 | // This selector might need adjustment based on the actual structure. 382 | // Let's try finding 'pre' elements and their preceding headings. 383 | $("pre").each((_, element) => { 384 | const codeBlock = $(element); 385 | const code = codeBlock.text().trim(); 386 | 387 | if (code) { 388 | let title = "Code Example"; 389 | let description : string | undefined = undefined; 390 | 391 | // Try to find the nearest preceding heading (h2, h3) 392 | let potentialTitleElement = codeBlock.closest('div[class*="relative"]').prev('h2, h3'); // Adjust selector based on actual structure 393 | if (!potentialTitleElement.length) { 394 | potentialTitleElement = codeBlock.parent().prev('h2, h3'); // Try another common structure 395 | } 396 | if (!potentialTitleElement.length) { 397 | potentialTitleElement = codeBlock.prev('h2, h3'); // Simplest case 398 | } 399 | 400 | 401 | if (potentialTitleElement.length) { 402 | title = potentialTitleElement.text().trim(); 403 | description = `Example for ${title}`; 404 | } else { 405 | // Fallback: Try to find a title in the code block structure if tabs are used 406 | const tabButton = codeBlock.closest('[role="tabpanel"]')?.attr('aria-labelledby'); 407 | if (tabButton) { 408 | const titleElement = $(`#${tabButton}`); 409 | if(titleElement.length && titleElement.text().trim().toLowerCase() === 'code') { 410 | // Find the heading associated with this example block 411 | let heading = $(`#${tabButton}`).closest('div').prev('h2, h3'); // Adjust based on DOM 412 | if(heading.length) title = heading.text().trim(); 413 | } 414 | } 415 | } 416 | 417 | 418 | examples.push({ title, code, description }); 419 | } 420 | }); 421 | 422 | // Deduplicate examples based on code content if necessary (simple check) 423 | const uniqueExamples = Array.from(new Map(examples.map(e => [e.code, e])).values()); 424 | 425 | console.error(`Found ${uniqueExamples.length} examples.`); 426 | return uniqueExamples; 427 | } 428 | 429 | /** 430 | * Finds the import statement within the extracted examples 431 | */ 432 | private findImportStatement(examples: ComponentExample[]): string | undefined { 433 | for (const example of examples) { 434 | const match = example.code.match(/import\s+{.*}\s+from\s+['"]@fluxui\/core['"]\;?/); 435 | if (match) { 436 | return match[0]; 437 | } 438 | } 439 | // Fallback search in case it's structured differently 440 | for (const example of examples) { 441 | const match = example.code.match(/import\s+.*\s+from\s+['"]@fluxui\/.*['"]\;?/); 442 | if (match) { 443 | return match[0]; 444 | } 445 | } 446 | console.error("Import statement not found in examples."); 447 | return undefined; 448 | } 449 | 450 | /** 451 | * Extracts component props from the API Reference section 452 | */ 453 | private extractProps($: cheerio.CheerioAPI): Record { 454 | const allProps: Record = {}; 455 | console.error("Looking for API Reference section..."); 456 | 457 | // Find the "API Reference" heading 458 | const apiSection = $("h2").filter((_, el) => $(el).text().trim() === "API Reference"); 459 | 460 | if (!apiSection.length) { 461 | console.error("API Reference section (h2) not found."); 462 | return allProps; 463 | } 464 | console.error("API Reference section found."); 465 | 466 | 467 | // Find all component headings (h3) and tables following the API Reference h2 468 | // This assumes structure: h2 -> h3 (Component Name) -> table (Props) 469 | apiSection.nextAll('h3').each((_, h3) => { 470 | const componentNameElement = $(h3); 471 | const componentName = componentNameElement.text().trim(); 472 | console.error(`Processing component: ${componentName}`); 473 | 474 | // Find the next table sibling for this h3 475 | const tableElement = componentNameElement.next('table').first(); // Or use nextUntil('h3').filter('table') 476 | 477 | if (!tableElement.length) { 478 | console.error(`Props table not found immediately after h3: ${componentName}`); 479 | // Try looking within a div sibling if structure is different 480 | const nextDiv = componentNameElement.next('div'); 481 | const tableInDiv = nextDiv.find('table').first(); 482 | if(!tableInDiv.length){ 483 | console.error(`Props table not found within next div for h3: ${componentName}`); 484 | return; // continue to next h3 485 | } 486 | // Found table within div, proceed with tableInDiv 487 | console.error(`Props table found within div for: ${componentName}`); 488 | this.processPropsTable($, tableInDiv, componentName, allProps); 489 | 490 | } else { 491 | console.error(`Props table found for: ${componentName}`); 492 | this.processPropsTable($, tableElement, componentName, allProps); 493 | } 494 | 495 | }); 496 | 497 | 498 | return allProps; 499 | } 500 | 501 | private processPropsTable($: cheerio.CheerioAPI, tableElement: cheerio.Cheerio, componentName: string, allProps: Record): void { 502 | const componentProps: ComponentProp[] = []; 503 | const headers: string[] = []; 504 | tableElement.find('thead th').each((_, th) => { 505 | headers.push($(th).text().trim().toLowerCase()); 506 | }); 507 | 508 | const propIndex = headers.indexOf('prop'); 509 | const typeIndex = headers.indexOf('type'); 510 | const defaultIndex = headers.indexOf('default'); 511 | // Add descriptionIndex if a description column exists 512 | // const descriptionIndex = headers.indexOf('description'); 513 | 514 | if (propIndex === -1 || typeIndex === -1) { 515 | console.error(`Could not find 'prop' or 'type' columns in table for ${componentName}`); 516 | return; // Skip this table 517 | } 518 | 519 | tableElement.find('tbody tr').each((_, tr) => { 520 | const cells = $(tr).find('td'); 521 | const propName = cells.eq(propIndex).text().trim(); 522 | const propType = cells.eq(typeIndex).text().trim(); 523 | const propDefault = defaultIndex !== -1 ? cells.eq(defaultIndex).text().trim() : undefined; 524 | // const propDescription = descriptionIndex !== -1 ? cells.eq(descriptionIndex).text().trim() : undefined; 525 | 526 | if (propName) { 527 | componentProps.push({ 528 | name: propName, 529 | type: propType, 530 | default: propDefault || undefined, // Ensure empty string becomes undefined 531 | // description: propDescription || undefined, 532 | }); 533 | } 534 | }); 535 | if (componentProps.length > 0) { 536 | allProps[componentName] = componentProps; 537 | console.error(`Extracted ${componentProps.length} props for ${componentName}`); 538 | } 539 | } 540 | 541 | 542 | /** 543 | * Handle the get_component_examples tool request 544 | */ 545 | private async handleGetComponentExamples(args: any) { 546 | const componentName = this.validateComponentName(args); 547 | 548 | try { 549 | // Use cached details if available, otherwise fetch 550 | let componentInfo: ComponentInfo | undefined = this.componentCache.get(componentName); 551 | if (!componentInfo) { 552 | console.error(`Cache miss for examples: ${componentName}, fetching details...`); 553 | componentInfo = await this.fetchComponentDetails(componentName); 554 | this.componentCache.set(componentName, componentInfo); // Cache the fetched details 555 | console.error(`Cached details while fetching examples for ${componentName}`); 556 | } else { 557 | console.error(`Cache hit for examples: ${componentName}`); 558 | } 559 | 560 | 561 | const examples = componentInfo?.examples || []; 562 | 563 | if (!examples || examples.length === 0) { 564 | console.error(`No examples found for ${componentName} even after fetch.`); 565 | // Optionally, you could try re-fetching just the examples part if details fetch failed previously 566 | // const freshExamples = await this.fetchComponentExamplesDirectly(componentName); 567 | // return this.createSuccessResponse(freshExamples); 568 | return this.createSuccessResponse([]); // Return empty array if none found 569 | } 570 | 571 | return this.createSuccessResponse(examples); 572 | } catch (error) { 573 | console.error(`Error fetching examples for ${componentName}:`, error); 574 | if (error instanceof McpError) { 575 | throw error; 576 | } 577 | // Pass specific context to error handler 578 | this.handleAxiosError(error, `fetching examples for component "${componentName}"`); 579 | } 580 | } 581 | 582 | 583 | // Optional: Direct fetch for examples if needed as fallback or separate logic 584 | // private async fetchComponentExamplesDirectly(componentName: string): Promise { 585 | // const componentUrl = `${this.FLUX_DOCS_URL}/components/${componentName}`; 586 | // const response = await this.axiosInstance.get(componentUrl); 587 | // const $ = cheerio.load(response.data); 588 | // return this.extractExamples($); 589 | // } 590 | 591 | 592 | /** 593 | * Handle the search_components tool request 594 | */ 595 | private async handleSearchComponents(args: any) { 596 | const query = this.validateSearchQuery(args); 597 | 598 | try { 599 | // Ensure components list is loaded 600 | await this.ensureComponentsListLoaded(); 601 | 602 | // Filter components matching the search query 603 | const results = this.searchComponentsByQuery(query); 604 | console.error(`Search for "${query}" found ${results.length} components.`); 605 | 606 | // Consider fetching full details for search results if needed, 607 | // but for now, just return name and URL like listComponents. 608 | // Or fetch descriptions if not already cached? 609 | const detailedResults = []; 610 | for (const component of results) { 611 | let details = this.componentCache.get(component.name); 612 | if (!details) { 613 | try { 614 | // Fetch details on demand for search results if not cached 615 | console.error(`Search cache miss for ${component.name}, fetching...`); 616 | details = await this.fetchComponentDetails(component.name); 617 | this.componentCache.set(component.name, details); // Cache fetched details 618 | } catch (fetchError) { 619 | console.error(`Failed to fetch details for search result ${component.name}:`, fetchError); 620 | // Use basic info if fetch fails 621 | details = component; // Use the basic ComponentInfo from the list 622 | } 623 | } 624 | detailedResults.push({ 625 | name: details.name, 626 | description: details.description, 627 | url: details.url, 628 | }); 629 | } 630 | 631 | 632 | return this.createSuccessResponse(detailedResults); 633 | } catch (error) { 634 | console.error(`Error during search for "${query}":`, error); 635 | if (error instanceof McpError) { 636 | throw error; 637 | } 638 | this.handleAxiosError(error, `searching components with query "${query}"`); 639 | } 640 | } 641 | 642 | /** 643 | * Ensures the components list is loaded in cache 644 | */ 645 | private async ensureComponentsListLoaded(): Promise { 646 | if (!this.componentsListCache) { 647 | console.error("Component list cache miss, fetching..."); 648 | await this.handleListComponents(); // This fetches and caches the list 649 | } 650 | 651 | if (!this.componentsListCache) { 652 | console.error("Failed to load components list after fetch attempt."); 653 | throw new McpError( 654 | ErrorCode.InternalError, 655 | "Failed to load components list" 656 | ); 657 | } 658 | console.error("Component list cache ensured."); 659 | } 660 | 661 | /** 662 | * Searches components by query string (name and description) 663 | */ 664 | private searchComponentsByQuery(query: string): ComponentInfo[] { 665 | if (!this.componentsListCache) { 666 | console.error("Attempted searchComponentsByQuery with unloaded cache."); 667 | return []; // Should have been loaded by ensureComponentsListLoaded 668 | } 669 | 670 | const lowerCaseQuery = query.toLowerCase(); 671 | 672 | // Prioritize components where the name matches exactly or starts with the query 673 | const nameMatches = this.componentsListCache.filter(component => 674 | component.name.toLowerCase() === lowerCaseQuery || 675 | component.name.toLowerCase().startsWith(lowerCaseQuery) 676 | ); 677 | 678 | // Then, add components where the description contains the query, avoiding duplicates 679 | const descriptionMatches = this.componentsListCache.filter(component => { 680 | // Fetch description if not available in list cache 681 | // This might require fetching details for all components upfront or on-demand during search 682 | // For now, we assume description might be pre-fetched or fetched on demand elsewhere 683 | // Let's refine search to only use name if description isn't readily available 684 | // Or modify handleListComponents to fetch descriptions initially (slower startup) 685 | // Sticking to name-only search for now based on list cache content. 686 | // Revisit if description search is crucial and descriptions are fetched. 687 | return false; // Temporarily disable description search based on current list cache structure 688 | // component.description?.toLowerCase().includes(lowerCaseQuery) 689 | } 690 | ); 691 | 692 | // Combine and return 693 | // return [...nameMatches, ...descriptionMatches]; 694 | return nameMatches; // Return only name matches for now 695 | } 696 | 697 | 698 | /** 699 | * Run the server 700 | */ 701 | async run() { 702 | const transport = new StdioServerTransport(); 703 | await this.server.connect(transport); 704 | console.error("Flux UI MCP server running on stdio"); 705 | } 706 | } 707 | 708 | // Create and run the server 709 | const server = new FluxUiServer(); 710 | server.run().catch((error) => { 711 | console.error("Server failed to run:", error); 712 | process.exit(1); 713 | }); --------------------------------------------------------------------------------