shadcn-vue
Version:
Add components to your apps.
304 lines (291 loc) • 13 kB
JavaScript
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