Skip to main content
Glama
reuvenaor

Shadcn Registry manager

by reuvenaor
index.ts8.65 kB
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { addItem } from "./mcp/handlers/add-item"; import { executeAdd } from "./mcp/handlers/execute-add"; import { executeInit } from "./mcp/handlers/execute-init"; import { getItem } from "./mcp/handlers/get-item"; import { getItems } from "./mcp/handlers/get-items"; import { getInitInstructions } from "./mcp/handlers/get-init-instructions"; import { addItemOptionsSchema, executeAddOptionsSchema, executeInitOptionsSchema, getItemOptionsSchema, } from '@/src/schemas'; import { validateRegistryUrl } from "./utils/registry-security"; import { getBlocks } from "./mcp/handlers/get-blocks"; async function main() { console.error("[MCP] Starting shadcn MCP server..."); const server = new McpServer( { name: "shadcn", version: "1.0.0", }, { capabilities: { logging: {}, resources: {}, tools: { listChanged: false, }, }, } ); // Security configuration const MAX_CONCURRENT_OPERATIONS = 5; let activeOperations = 0; // Wrapper function to enforce rate limiting function withRateLimit<T extends any[], R>(fn: (...args: T) => Promise<R>) { return async (...args: T): Promise<R> => { if (activeOperations >= MAX_CONCURRENT_OPERATIONS) { throw new Error("Too many concurrent operations. Please try again later."); } activeOperations++; try { return await fn(...args); } finally { activeOperations--; } }; } // Validate and set the registry URL from environment with security checks let REGISTRY_URL: string; try { const envUrl = process.env.REGISTRY_URL ?? "http://host.docker.internal:3333/r"; REGISTRY_URL = validateRegistryUrl(envUrl); } catch (error) { console.error(`[MCP] Security error - Invalid REGISTRY_URL: ${error instanceof Error ? error.message : String(error)}`); console.error(`[MCP] Falling back to secure default`); REGISTRY_URL = "http://host.docker.internal:3333/r"; // Safe fallback } // Validate style parameter const STYLE = (() => { const envStyle = process.env.STYLE ?? "new-york"; if (typeof envStyle === 'string' && /^[a-z-]+$/i.test(envStyle) && envStyle.length <= 50) { return envStyle; } console.warn(`[MCP] Invalid STYLE environment variable, using default: new-york`); return "new-york"; })(); // Register tools directly with MCP SDK handlers (bypassing wrapHandler due to type incompatibility) server.registerTool( "get_init_instructions", { description: "Get instructions on how to initialize a new project using a registry style project structure.", inputSchema: z.object({}).shape, }, async (args, extra) => { const result = await getInitInstructions({ registryUrl: REGISTRY_URL, style: STYLE, }); return { content: result.content.map((c) => ({ type: "text" as const, text: c.text || "", _meta: {} })) }; } ); server.registerTool( "execute_init", { description: "Execute the full init workflow - this actually initializes the project and creates a components.json file.", inputSchema: executeInitOptionsSchema.shape, }, withRateLimit(async (args, extra) => { const result = await executeInit(args as z.infer<typeof executeInitOptionsSchema>, extra); return { content: result.content.map((c) => { if ('resource' in c && c.resource) { return { type: "resource" as const, resource: { uri: c.resource.uri, text: c.resource.text, mimeType: "text/plain", _meta: {} } }; } else { return { type: "text" as const, text: c.text || "", _meta: {} }; } }), structuredContent: result.structuredContent, isError: result.isError }; }) ); server.registerTool( "get_items", { description: "List all the available items in the registry", inputSchema: z.object({}).shape, }, async (args, extra) => { const result = await getItems({ registryUrl: REGISTRY_URL }); return { content: result.content.map((c) => ({ type: "text" as const, text: c.text || "", _meta: {} })) }; } ); // Apply to ALL handlers that return content: server.registerTool( "get_item", { description: "Get an item from the registry", inputSchema: getItemOptionsSchema.shape, }, async (args, extra) => { const result = await getItem(args as z.infer<typeof getItemOptionsSchema>, { registryUrl: REGISTRY_URL, style: STYLE, }); return { content: result.content.map((c) => ({ type: "text" as const, text: c.text || "", _meta: {} })), structuredContent: result.structuredContent || undefined }; } ); server.registerTool( "add_item", { description: "Add an item from the registry to the user's project", inputSchema: addItemOptionsSchema.shape, }, async (args, extra) => { const result = await addItem(args as z.infer<typeof addItemOptionsSchema>, extra); return { content: result.content.map((c) => { if ('resource' in c && c.resource) { return { type: "resource" as const, resource: { uri: c.resource.uri, text: c.resource.text, mimeType: "text/plain", _meta: {} } }; } else { return { type: "text" as const, text: (c as { text?: string }).text || "", _meta: {} }; } }), structuredContent: result.structuredContent, isError: result.isError }; } ); server.registerTool( "execute_add", { description: "Execute the full add component workflow - this actually adds the component to the user's project instead of just providing instructions", inputSchema: executeAddOptionsSchema.shape, }, withRateLimit(async (args, extra) => { const result = await executeAdd(args as z.infer<typeof executeAddOptionsSchema>, extra); return { content: result.content.map((c) => { if ('resource' in c && c.resource) { return { type: "resource" as const, resource: { uri: c.resource.uri, text: c.resource.text, mimeType: "text/plain", _meta: {} } }; } else { return { type: "text" as const, text: (c as { text?: string }).text || "", _meta: {} }; } }), structuredContent: result.structuredContent, isError: result.isError }; }) ); server.registerTool( "get_blocks", { description: "List all the available blocks from the local blocks.json file", inputSchema: z.object({}).shape, }, async (_args, _extra) => { const blocks = await getBlocks(); return { content: [ { type: "text" as const, text: `Blocks available:\n${blocks.map((b: { name: string }) => `- ${b.name}`).join("\n")}`, _meta: {} } ], structuredContent: { blocks } }; } ); console.error("[MCP] Tools registered successfully"); // Create stdio transport const transport = new StdioServerTransport(); console.error("[MCP] Created stdio transport"); // Connect the server to the transport await server.connect(transport); console.error("[MCP] Server connected to stdio transport"); // The server will now handle requests via stdin/stdout console.error("[MCP] MCP server ready for requests"); } // Handle errors and ensure proper shutdown process.on('SIGINT', () => { console.error("[MCP] Received SIGINT, shutting down..."); process.exit(0); }); process.on('SIGTERM', () => { console.error("[MCP] Received SIGTERM, shutting down..."); process.exit(0); }); main().catch((error) => { console.error("[MCP] Fatal error:", error); process.exit(1); });

Latest Blog Posts

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/reuvenaor/shadcn-registry-manager'

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