mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-07-02 00:54:15 +00:00
feat(shadcn): mcp init (#8086)
This commit is contained in:
5
.changeset/shiny-moles-pull.md
Normal file
5
.changeset/shiny-moles-pull.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"shadcn": major
|
||||
---
|
||||
|
||||
add mcp init command
|
||||
@@ -1,13 +1,70 @@
|
||||
import { promises as fs } from "fs"
|
||||
import path from "path"
|
||||
import { server } from "@/src/mcp"
|
||||
import { loadEnvFiles } from "@/src/utils/env-loader"
|
||||
import { getConfig } from "@/src/utils/get-config"
|
||||
import { getPackageManager } from "@/src/utils/get-package-manager"
|
||||
import { handleError } from "@/src/utils/handle-error"
|
||||
import { logger } from "@/src/utils/logger"
|
||||
import { spinner } from "@/src/utils/spinner"
|
||||
import { updateDependencies } from "@/src/utils/updaters/update-dependencies"
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
||||
import { Command } from "commander"
|
||||
import deepmerge from "deepmerge"
|
||||
import { execa } from "execa"
|
||||
import fsExtra from "fs-extra"
|
||||
import prompts from "prompts"
|
||||
import z from "zod"
|
||||
|
||||
const SHADCN_MCP_VERSION = "beta"
|
||||
|
||||
const CLIENTS = [
|
||||
{
|
||||
name: "claude",
|
||||
label: "Claude Code",
|
||||
configPath: ".mcp.json",
|
||||
config: {
|
||||
mcpServers: {
|
||||
shadcn: {
|
||||
command: "npx",
|
||||
args: [`shadcn@${SHADCN_MCP_VERSION}`, "mcp"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cursor",
|
||||
label: "Cursor",
|
||||
configPath: ".cursor/mcp.json",
|
||||
config: {
|
||||
mcpServers: {
|
||||
shadcn: {
|
||||
command: "npx",
|
||||
args: [`shadcn@${SHADCN_MCP_VERSION}`, "mcp"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "vscode",
|
||||
label: "VS Code",
|
||||
configPath: ".vscode/mcp.json",
|
||||
config: {
|
||||
servers: {
|
||||
shadcn: {
|
||||
command: "npx",
|
||||
args: [`shadcn@${SHADCN_MCP_VERSION}`, "mcp"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const
|
||||
|
||||
const DEPENDENCIES = [`shadcn@${SHADCN_MCP_VERSION}`]
|
||||
|
||||
export const mcp = new Command()
|
||||
.name("mcp")
|
||||
.description("starts the registry MCP server")
|
||||
.description("MCP server and configuration commands")
|
||||
.option(
|
||||
"-c, --cwd <cwd>",
|
||||
"the working directory. defaults to the current directory.",
|
||||
@@ -23,3 +80,119 @@ export const mcp = new Command()
|
||||
handleError(error)
|
||||
}
|
||||
})
|
||||
|
||||
const mcpInitOptionsSchema = z.object({
|
||||
client: z.enum(["claude", "cursor", "vscode"]),
|
||||
cwd: z.string(),
|
||||
})
|
||||
|
||||
mcp
|
||||
.command("init")
|
||||
.description("Initialize MCP configuration for your client")
|
||||
.option(
|
||||
"--client <client>",
|
||||
`MCP client (${CLIENTS.map((c) => c.name).join(", ")})`
|
||||
)
|
||||
.action(async (opts, command) => {
|
||||
try {
|
||||
// Get the cwd from parent command.
|
||||
const parentOpts = command.parent?.opts() || {}
|
||||
const cwd = parentOpts.cwd || process.cwd()
|
||||
|
||||
let client = opts.client
|
||||
|
||||
if (!client) {
|
||||
const response = await prompts({
|
||||
type: "select",
|
||||
name: "client",
|
||||
message: "Which MCP client are you using?",
|
||||
choices: CLIENTS.map((c) => ({
|
||||
title: c.label,
|
||||
value: c.name,
|
||||
})),
|
||||
})
|
||||
|
||||
if (!response.client) {
|
||||
logger.break()
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
client = response.client
|
||||
}
|
||||
|
||||
const options = mcpInitOptionsSchema.parse({
|
||||
client,
|
||||
cwd,
|
||||
})
|
||||
|
||||
const configSpinner = spinner("Configuring MCP server...").start()
|
||||
const configPath = await runMcpInit(options)
|
||||
configSpinner.succeed("Configuring MCP server.")
|
||||
|
||||
const config = await getConfig(options.cwd)
|
||||
if (config) {
|
||||
await updateDependencies([], DEPENDENCIES, config, {
|
||||
silent: false,
|
||||
})
|
||||
} else {
|
||||
const packageManager = await getPackageManager(options.cwd)
|
||||
const installCommand = packageManager === "npm" ? "install" : "add"
|
||||
const devFlag = packageManager === "npm" ? "--save-dev" : "-D"
|
||||
|
||||
const installSpinner = spinner("Installing dependencies...").start()
|
||||
await execa(
|
||||
packageManager,
|
||||
[installCommand, devFlag, ...DEPENDENCIES],
|
||||
{
|
||||
cwd: options.cwd,
|
||||
}
|
||||
)
|
||||
installSpinner.succeed("Installing dependencies.")
|
||||
}
|
||||
|
||||
logger.break()
|
||||
logger.success(`Configuration saved to ${configPath}.`)
|
||||
logger.break()
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
}
|
||||
})
|
||||
|
||||
const overwriteMerge = (_: any[], sourceArray: any[]) => sourceArray
|
||||
|
||||
async function runMcpInit(options: z.infer<typeof mcpInitOptionsSchema>) {
|
||||
const { client, cwd } = options
|
||||
|
||||
const clientInfo = CLIENTS.find((c) => c.name === client)
|
||||
if (!clientInfo) {
|
||||
throw new Error(
|
||||
`Unknown client: ${client}. Available clients: ${CLIENTS.map(
|
||||
(c) => c.name
|
||||
).join(", ")}`
|
||||
)
|
||||
}
|
||||
|
||||
const configPath = path.join(cwd, clientInfo.configPath)
|
||||
|
||||
let existingConfig = {}
|
||||
try {
|
||||
const content = await fs.readFile(configPath, "utf-8")
|
||||
existingConfig = JSON.parse(content)
|
||||
} catch {}
|
||||
|
||||
const mergedConfig = deepmerge(
|
||||
existingConfig,
|
||||
clientInfo.config as Record<string, unknown>,
|
||||
{ arrayMerge: overwriteMerge }
|
||||
)
|
||||
|
||||
const dir = path.dirname(configPath)
|
||||
await fsExtra.ensureDir(dir)
|
||||
await fs.writeFile(
|
||||
configPath,
|
||||
JSON.stringify(mergedConfig, null, 2) + "\n",
|
||||
"utf-8"
|
||||
)
|
||||
|
||||
return clientInfo.configPath
|
||||
}
|
||||
|
||||
@@ -135,6 +135,12 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
})
|
||||
),
|
||||
},
|
||||
{
|
||||
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({})),
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
@@ -380,6 +386,27 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
}
|
||||
}
|
||||
|
||||
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`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user