266 lines
6.9 KiB
TypeScript
266 lines
6.9 KiB
TypeScript
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 { highlighter } from "@/src/utils/highlighter"
|
|
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 = "latest"
|
|
|
|
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"],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "codex",
|
|
label: "Codex",
|
|
configPath: ".codex/config.toml",
|
|
config: `[mcp_servers.shadcn]
|
|
command = "npx"
|
|
args = ["shadcn@${SHADCN_MCP_VERSION}", "mcp"]
|
|
`,
|
|
},
|
|
{
|
|
name: "opencode",
|
|
label: "OpenCode",
|
|
configPath: "opencode.json",
|
|
config: {
|
|
$schema: "https://opencode.ai/config.json",
|
|
mcp: {
|
|
shadcn: {
|
|
type: "local",
|
|
command: ["npx", `shadcn@${SHADCN_MCP_VERSION}`, "mcp"],
|
|
enabled: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
] as const
|
|
|
|
const DEPENDENCIES = [`shadcn@${SHADCN_MCP_VERSION}`]
|
|
|
|
export const mcp = new Command()
|
|
.name("mcp")
|
|
.description("MCP server and configuration commands")
|
|
.option(
|
|
"-c, --cwd <cwd>",
|
|
"the working directory. defaults to the current directory.",
|
|
process.cwd()
|
|
)
|
|
.action(async (options) => {
|
|
try {
|
|
await loadEnvFiles(options.cwd)
|
|
const transport = new StdioServerTransport()
|
|
await server.connect(transport)
|
|
} catch (error) {
|
|
logger.break()
|
|
handleError(error)
|
|
}
|
|
})
|
|
|
|
const mcpInitOptionsSchema = z.object({
|
|
client: z.enum(["claude", "cursor", "vscode", "codex", "opencode"]),
|
|
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 config = await getConfig(options.cwd)
|
|
|
|
if (options.client === "codex") {
|
|
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.log("To configure the shadcn MCP server in Codex:")
|
|
logger.break()
|
|
logger.log(
|
|
`1. Open or create the file ${highlighter.info(
|
|
"~/.codex/config.toml"
|
|
)}`
|
|
)
|
|
logger.log("2. Add the following configuration:")
|
|
logger.log()
|
|
logger.info(`[mcp_servers.shadcn]
|
|
command = "npx"
|
|
args = ["shadcn@${SHADCN_MCP_VERSION}", "mcp"]`)
|
|
logger.break()
|
|
logger.info("3. Restart Codex to load the MCP server")
|
|
logger.break()
|
|
process.exit(0)
|
|
}
|
|
|
|
const configSpinner = spinner("Configuring MCP server...").start()
|
|
const configPath = await runMcpInit(options)
|
|
configSpinner.succeed("Configuring MCP server.")
|
|
|
|
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)
|
|
const dir = path.dirname(configPath)
|
|
await fsExtra.ensureDir(dir)
|
|
|
|
// Handle JSON format.
|
|
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 }
|
|
)
|
|
|
|
await fs.writeFile(
|
|
configPath,
|
|
JSON.stringify(mergedConfig, null, 2) + "\n",
|
|
"utf-8"
|
|
)
|
|
|
|
return clientInfo.configPath
|
|
}
|