Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ npx @magicuidesign/cli@latest install <client>
- [x] claude
- [x] cline
- [x] roo-cline
- [x] vscode

## Manual Installation

Expand Down
15 changes: 11 additions & 4 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ const execAsync = promisify(exec);
async function isClientRunning(client: string): Promise<boolean> {
try {
const platform = process.platform;
const clientProcess = { claude: "Claude" }[client] || client;
const clientProcess =
{ claude: "Claude", vscode: "Code" }[client] || client;

if (platform === "win32") {
const { stdout } = await execAsync(
Expand All @@ -31,14 +32,18 @@ async function isClientRunning(client: string): Promise<boolean> {
}

async function restartClient(client: string): Promise<void> {
const clientProcess = { claude: "Claude" }[client] || client;
const clientProcess =
{ claude: "Claude", vscode: "Code" }[client] || client;
const platform = process.platform;
const vscodeDarwinAppName = "Visual Studio Code";

try {
if (platform === "win32") {
await execAsync(`taskkill /F /IM "${clientProcess}.exe"`);
} else if (platform === "darwin") {
await execAsync(`killall "${clientProcess}"`);
const killTarget =
client === "vscode" ? "Code" : clientProcess;
await execAsync(`killall "${killTarget}"`);
} else if (platform === "linux") {
await execAsync(`pkill -f "${clientProcess.toLowerCase()}"`);
}
Expand All @@ -48,7 +53,9 @@ async function restartClient(client: string): Promise<void> {
if (platform === "win32") {
await execAsync(`start "" "${clientProcess}.exe"`);
} else if (platform === "darwin") {
await execAsync(`open -a "${clientProcess}"`);
const openTarget =
client === "vscode" ? vscodeDarwinAppName : clientProcess;
await execAsync(`open -a "${openTarget}"`);
} else if (platform === "linux") {
await execAsync(clientProcess.toLowerCase());
}
Expand Down
19 changes: 17 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os from "node:os";
import path from "node:path";
import type { ValidClient, VscodeClientConfig, McpServersClientConfig } from "./types.js";

const homeDir = os.homedir();

Expand Down Expand Up @@ -39,6 +40,7 @@ export const clientPaths: Record<string, string> = {
),
windsurf: path.join(homeDir, ".codeium", "windsurf", "mcp_config.json"),
cursor: path.join(homeDir, ".cursor", "mcp.json"),
vscode: path.join(baseDir, "Code", "User", "mcp.json"),
};

export const createPlatformCommand = (passedArgs: string[]) => {
Expand All @@ -54,7 +56,20 @@ export const createPlatformCommand = (passedArgs: string[]) => {
};
};

export const getDefaultConfig = () => {
export function getDefaultConfig(
client: ValidClient,
): McpServersClientConfig | VscodeClientConfig {
if (client === "vscode") {
return {
servers: {
magicui: {
command: "npx",
args: ["-y", "@magicuidesign/mcp@latest"],
},
},
};
}

const args = ["-y", "@magicuidesign/mcp@latest"];
const command = createPlatformCommand(args);

Expand All @@ -63,4 +78,4 @@ export const getDefaultConfig = () => {
"@magicuidesign/mcp": command,
},
};
};
}
10 changes: 8 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,21 @@ import { getDefaultConfig } from "./config.js";
import type { ValidClient } from "./types.js";
import { writeConfig } from "./utils.js";

const CLIENT_DISPLAY_NAMES: Partial<Record<ValidClient, string>> = {
vscode: "VS Code",
};

export async function install(client: ValidClient): Promise<void> {
const capitalizedClient = client.charAt(0).toUpperCase() + client.slice(1);
const capitalizedClient =
CLIENT_DISPLAY_NAMES[client] ??
client.charAt(0).toUpperCase() + client.slice(1);

const spinner = ora(
`Installing configuration for ${capitalizedClient}...`,
).start();

try {
const config = { ...getDefaultConfig() };
const config = getDefaultConfig(client);

writeConfig(client, config);
spinner.succeed(
Expand Down
14 changes: 12 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,35 @@ export type ValidClient =
| "cline"
| "roo-cline"
| "windsurf"
| "cursor";
| "cursor"
| "vscode";

export const VALID_CLIENTS: ValidClient[] = [
"claude",
"cline",
"roo-cline",
"windsurf",
"cursor",
"vscode",
];

export interface ServerConfig {
command: string;
args: string[];
}

export interface ClientConfig {
/** MCP config shape used by Claude Desktop, Cursor, Windsurf, Cline, etc. */
export interface McpServersClientConfig {
mcpServers: Record<string, ServerConfig>;
}

/** VS Code native MCP config (`User/mcp.json`) */
export interface VscodeClientConfig {
servers: Record<string, ServerConfig>;
}

export type ClientConfig = McpServersClientConfig | VscodeClientConfig;

export interface InstallOptions {
apiKey?: string;
}
65 changes: 59 additions & 6 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import fs from "node:fs";
import path from "node:path";
import type { ValidClient, ClientConfig } from "./types.js";
import type {
ValidClient,
ClientConfig,
McpServersClientConfig,
VscodeClientConfig,
} from "./types.js";
import { clientPaths } from "./config.js";

export function getConfigPath(client: ValidClient): string {
Expand All @@ -15,17 +20,30 @@ export function readConfig(client: ValidClient): ClientConfig {
const configPath = getConfigPath(client);

if (!fs.existsSync(configPath)) {
return { mcpServers: {} };
return client === "vscode"
? { servers: {} }
: { mcpServers: {} };
}

try {
const rawConfig = JSON.parse(fs.readFileSync(configPath, "utf8"));
if (client === "vscode") {
return {
...rawConfig,
servers:
rawConfig.servers && typeof rawConfig.servers === "object"
? rawConfig.servers
: {},
};
}
return {
...rawConfig,
mcpServers: rawConfig.mcpServers || {},
};
} catch (error) {
return { mcpServers: {} };
return client === "vscode"
? { servers: {} }
: { mcpServers: {} };
}
}

Expand All @@ -37,11 +55,46 @@ export function writeConfig(client: ValidClient, config: ClientConfig): void {
fs.mkdirSync(configDir, { recursive: true });
}

if (!config.mcpServers || typeof config.mcpServers !== "object") {
if (client === "vscode") {
const vscodeConfig = config as VscodeClientConfig;
if (!vscodeConfig.servers || typeof vscodeConfig.servers !== "object") {
throw new Error("Invalid servers structure");
}

let existingRaw: Record<string, unknown> = {};
try {
if (fs.existsSync(configPath)) {
existingRaw = JSON.parse(fs.readFileSync(configPath, "utf8"));
}
} catch {
// If reading fails, continue with empty existing config
}

const existingServers =
existingRaw.servers &&
typeof existingRaw.servers === "object" &&
!Array.isArray(existingRaw.servers)
? (existingRaw.servers as Record<string, unknown>)
: {};

const mergedConfig = {
...existingRaw,
servers: {
...existingServers,
...vscodeConfig.servers,
},
};

fs.writeFileSync(configPath, JSON.stringify(mergedConfig, null, 2));
return;
}

const mcpConfig = config as McpServersClientConfig;
if (!mcpConfig.mcpServers || typeof mcpConfig.mcpServers !== "object") {
throw new Error("Invalid mcpServers structure");
}

let existingConfig: ClientConfig = { mcpServers: {} };
let existingConfig: McpServersClientConfig = { mcpServers: {} };
try {
if (fs.existsSync(configPath)) {
existingConfig = JSON.parse(fs.readFileSync(configPath, "utf8"));
Expand All @@ -54,7 +107,7 @@ export function writeConfig(client: ValidClient, config: ClientConfig): void {
...existingConfig,
mcpServers: {
...existingConfig.mcpServers,
...config.mcpServers,
...mcpConfig.mcpServers,
},
};

Expand Down