UNPKG

shadcn-vue

Version:
304 lines (291 loc) 13 kB
import { RegistryError, getRegistriesConfig, getRegistryItems, searchRegistries } from "./registry-CVURNCtV.js"; import { z } from "zod"; import { detectPackageManager } from "nypm"; import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"; import dedent from "dedent"; import { zodToJsonSchema } from "zod-to-json-schema"; //#region src/mcp/utils.ts const SHADCN_CLI_COMMAND = "shadcn-vue"; async function npxShadcnVue(command) { return `${(await detectPackageManager(process.cwd()))?.name} ${SHADCN_CLI_COMMAND} ${command}`; } async function getMcpConfig(cwd = process.cwd()) { return { registries: (await getRegistriesConfig(cwd)).registries }; } function formatSearchResultsWithPagination(results, options) { const { query, registries } = options || {}; const formattedItems = results.items.map((item) => { const parts = [`- ${item.name}`]; if (item.type) parts.push(`(${item.type})`); if (item.description) parts.push(`- ${item.description}`); if (item.registry) parts.push(`[${item.registry}]`); parts.push(`\n Add command: \`${npxShadcnVue(`add ${item.addCommandArgument}`)}\``); return parts.join(" "); }); let header = `Found ${results.pagination.total} items`; if (query) header += ` matching "${query}"`; if (registries && registries.length > 0) header += ` in registries ${registries.join(", ")}`; header += ":"; const showingRange = `Showing items ${results.pagination.offset + 1}-${Math.min(results.pagination.offset + results.pagination.limit, results.pagination.total)} of ${results.pagination.total}:`; let output = `${header}\n\n${showingRange}\n\n${formattedItems.join("\n\n")}`; if (results.pagination.hasMore) output += `\n\nMore items available. Use offset: ${results.pagination.offset + results.pagination.limit} to see the next page.`; return output; } function formatRegistryItems(items) { return items.map((item) => { return [ `## ${item.name}`, item.description ? `\n${item.description}\n` : "", item.type ? `**Type:** ${item.type}` : "", item.files && item.files.length > 0 ? `**Files:** ${item.files.length} file(s)` : "", item.dependencies && item.dependencies.length > 0 ? `**Dependencies:** ${item.dependencies.join(", ")}` : "", item.devDependencies && item.devDependencies.length > 0 ? `**Dev Dependencies:** ${item.devDependencies.join(", ")}` : "" ].filter(Boolean).join("\n"); }); } function formatItemExamples(items, query) { const sections = items.map((item) => { const parts = [`## Example: ${item.name}`, item.description ? `\n${item.description}\n` : ""]; if (item.files?.length) item.files.forEach((file) => { if (file.content) { parts.push(`### Code (${file.path}):\n`); parts.push("```tsx"); parts.push(file.content); parts.push("```"); } }); return parts.filter(Boolean).join("\n"); }); return `# Usage Examples\n\nFound ${items.length} example${items.length > 1 ? "s" : ""} matching "${query}":\n` + sections.join("\n\n---\n\n"); } //#endregion //#region src/mcp/index.ts const server = new Server({ name: "shadcnVue", version: "1.0.0" }, { capabilities: { resources: {}, tools: {} } }); server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "get_project_registries", description: "Get configured registry names from components.json - Returns error if no components.json exists (use init_project to create one)", inputSchema: zodToJsonSchema(z.object({})) }, { name: "list_items_in_registries", description: "List items from registries (requires components.json - use init_project if missing)", inputSchema: zodToJsonSchema(z.object({ registries: z.array(z.string()).describe("Array of registry names to search (e.g., ['@shadcn', '@acme'])"), limit: z.number().optional().describe("Maximum number of items to return"), offset: z.number().optional().describe("Number of items to skip for pagination") })) }, { name: "search_items_in_registries", description: "Search for components in registries using fuzzy matching (requires components.json). After finding an item, use get_item_examples_from_registries to see usage examples.", inputSchema: zodToJsonSchema(z.object({ registries: z.array(z.string()).describe("Array of registry names to search (e.g., ['@shadcn', '@acme'])"), query: z.string().describe("Search query string for fuzzy matching against item names and descriptions"), limit: z.number().optional().describe("Maximum number of items to return"), offset: z.number().optional().describe("Number of items to skip for pagination") })) }, { name: "view_items_in_registries", description: "View detailed information about specific registry items including the name, description, type and files content. For usage examples, use get_item_examples_from_registries instead.", inputSchema: zodToJsonSchema(z.object({ items: z.array(z.string()).describe("Array of item names with registry prefix (e.g., ['@shadcn/button', '@shadcn/card'])") })) }, { name: "get_item_examples_from_registries", description: "Find usage examples and demos with their complete code. Search for patterns like 'accordion-demo', 'button example', 'card-demo', etc. Returns full implementation code with dependencies.", inputSchema: zodToJsonSchema(z.object({ registries: z.array(z.string()).describe("Array of registry names to search (e.g., ['@shadcn', '@acme'])"), query: z.string().describe("Search query for examples (e.g., 'accordion-demo', 'button demo', 'card example', 'tooltip-demo', 'example-booking-form', 'example-hero'). Common patterns: '{item-name}-demo', '{item-name} example', 'example {item-name}'") })) }, { name: "get_add_command_for_items", description: "Get the shadcn-vue CLI add command for specific items in a registry. This is useful for adding one or more components to your project.", inputSchema: zodToJsonSchema(z.object({ items: z.array(z.string()).describe("Array of items to get the add command for prefixed with the registry name (e.g., ['@shadcn/button', '@shadcn/card'])") })) }, { name: "get_audit_checklist", description: "After creating new components or generating new code files, use this tool for a quick checklist to verify that everything is working as expected. Make sure to run the tool after all required steps have been completed.", inputSchema: zodToJsonSchema(z.object({})) } ] }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { try { if (!request.params.arguments) throw new Error("No tool arguments provided."); switch (request.params.name) { case "get_project_registries": { const config = await getMcpConfig(process.cwd()); if (!config?.registries) return { content: [{ type: "text", text: dedent`No components.json found or no registries configured. To fix this: 1. Use the \`init\` command to create a components.json file 2. Or manually create components.json with a registries section` }] }; return { content: [{ type: "text", text: dedent`The following registries are configured in the current project: ${Object.keys(config.registries).map((registry) => `- ${registry}`).join("\n")} You can view the items in a registry by running: \`${await npxShadcnVue("view @name-of-registry")}\` For example: \`${await npxShadcnVue("view @shadcn")}\` or \`${await npxShadcnVue("view @shadcn @acme")}\` to view multiple registries. ` }] }; } case "search_items_in_registries": { const args = z.object({ registries: z.array(z.string()), query: z.string(), limit: z.number().optional(), offset: z.number().optional() }).parse(request.params.arguments); const results = await searchRegistries(args.registries, { query: args.query, limit: args.limit, offset: args.offset, config: await getMcpConfig(process.cwd()), useCache: false }); if (results.items.length === 0) return { content: [{ type: "text", text: dedent`No items found matching "${args.query}" in registries ${args.registries.join(", ")}, Try searching with a different query or registry.` }] }; return { content: [{ type: "text", text: formatSearchResultsWithPagination(results, { query: args.query, registries: args.registries }) }] }; } case "list_items_in_registries": { const args = z.object({ registries: z.array(z.string()), limit: z.number().optional(), offset: z.number().optional(), cwd: z.string().optional() }).parse(request.params.arguments); const results = await searchRegistries(args.registries, { limit: args.limit, offset: args.offset, config: await getMcpConfig(process.cwd()), useCache: false }); if (results.items.length === 0) return { content: [{ type: "text", text: dedent`No items found in registries ${args.registries.join(", ")}.` }] }; return { content: [{ type: "text", text: formatSearchResultsWithPagination(results, { registries: args.registries }) }] }; } case "view_items_in_registries": { const args = z.object({ items: z.array(z.string()) }).parse(request.params.arguments); const registryItems = await getRegistryItems(args.items, { config: await getMcpConfig(process.cwd()), useCache: false }); if (registryItems?.length === 0) return { content: [{ type: "text", text: dedent`No items found for: ${args.items.join(", ")} Make sure the item names are correct and include the registry prefix (e.g., @shadcn/button).` }] }; return { content: [{ type: "text", text: dedent`Item Details: ${formatRegistryItems(registryItems).join("\n\n---\n\n")}` }] }; } case "get_item_examples_from_registries": { const args = z.object({ query: z.string(), registries: z.array(z.string()) }).parse(request.params.arguments); const config = await getMcpConfig(); const results = await searchRegistries(args.registries, { query: args.query, config, useCache: false }); if (results.items.length === 0) return { content: [{ type: "text", text: dedent`No examples found for query "${args.query}". Try searching with patterns like: - "accordion-demo" for accordion examples - "button demo" or "button example" - Component name followed by "-demo" or "example" You can also: 1. Use search_items_in_registries to find all items matching your query 2. View the main component with view_items_in_registries for inline usage documentation` }] }; return { content: [{ type: "text", text: formatItemExamples(await getRegistryItems(results.items.map((item) => item.addCommandArgument), { config, useCache: false }), args.query) }] }; } case "get_add_command_for_items": return { content: [{ type: "text", text: await npxShadcnVue(`add ${z.object({ items: z.array(z.string()) }).parse(request.params.arguments).items.join(" ")}`) }] }; case "get_audit_checklist": return { content: [{ type: "text", text: dedent`## Component Audit Checklist After adding or generating components, check the following common issues: - [ ] Ensure imports are correct i.e named vs default imports - [ ] If using next/image, ensure images.remotePatterns next.config.js is configured correctly. - [ ] Ensure all dependencies are installed. - [ ] Check for linting errors or warnings - [ ] Check for TypeScript errors - [ ] Use the Playwright MCP if available. ` }] }; default: throw new Error(`Tool ${request.params.name} not found`); } } catch (error) { if (error instanceof z.ZodError) return { content: [{ type: "text", text: dedent`Invalid input parameters: ${error.errors.map((e) => `- ${e.path.join(".")}: ${e.message}`).join("\n")} ` }], isError: true }; if (error instanceof RegistryError) { let errorMessage = error.message; if (error.suggestion) errorMessage += `\n\n💡 ${error.suggestion}`; if (error.context) errorMessage += `\n\nContext: ${JSON.stringify(error.context, null, 2)}`; return { content: [{ type: "text", text: dedent`Error (${error.code}): ${errorMessage}` }], isError: true }; } return { content: [{ type: "text", text: dedent`Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } }); //#endregion export { server }; //# sourceMappingURL=mcp-ebpb1d4Z.js.map