@smithery/cli 0.0.25 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Types for entries in the Smithery registry
3
+ */
4
+ import { z } from "zod";
5
+ export const JSONSchemaSchema = z.lazy(() => z.object({
6
+ type: z.string().optional(),
7
+ properties: z.record(JSONSchemaSchema).optional(),
8
+ items: JSONSchemaSchema.optional(),
9
+ required: z.array(z.string()).optional(),
10
+ description: z.string().optional(),
11
+ default: z.unknown().optional(),
12
+ }));
13
+ export const ConnectionDetailsSchema = z.object({
14
+ type: z.enum(["stdio"]),
15
+ configSchema: JSONSchemaSchema.optional(),
16
+ exampleConfig: z.record(z.any()).optional(),
17
+ });
18
+ // stdio connection
19
+ export const StdioConnectionSchema = z.object({
20
+ command: z.string().describe("The executable to run to start the server."),
21
+ args: z
22
+ .array(z.string())
23
+ .optional()
24
+ .describe("Command line arguments to pass to the executable."),
25
+ env: z
26
+ .record(z.string(), z.string())
27
+ .optional()
28
+ .describe("The environment to use when spawning the process."),
29
+ });
@@ -0,0 +1,93 @@
1
+ import { exec } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import inquirer from "inquirer";
4
+ const execAsync = promisify(exec);
5
+ async function isClientRunning(client) {
6
+ if (!client)
7
+ return false;
8
+ try {
9
+ const platform = process.platform;
10
+ // Map of supported clients to their process names
11
+ // Currently only Claude is officially supported
12
+ // Other entries are placeholders for future integrations
13
+ const clientProcess = {
14
+ claude: "Claude",
15
+ // jan: "Jan", // Placeholder for future client integration
16
+ // Add more clients here as they become supported
17
+ }[client] || client;
18
+ if (platform === "win32") {
19
+ const { stdout } = await execAsync(`tasklist /FI "IMAGENAME eq ${clientProcess}.exe" /NH`);
20
+ return stdout.includes(`${clientProcess}.exe`);
21
+ }
22
+ else if (platform === "darwin") {
23
+ const { stdout } = await execAsync(`pgrep -x "${clientProcess}"`);
24
+ return !!stdout.trim();
25
+ }
26
+ else if (platform === "linux") {
27
+ const { stdout } = await execAsync(`pgrep -f "${clientProcess.toLowerCase()}"`);
28
+ return !!stdout.trim();
29
+ }
30
+ return false;
31
+ }
32
+ catch (error) {
33
+ // If the command fails, assume client is not running
34
+ return false;
35
+ }
36
+ }
37
+ async function restartClient(client) {
38
+ const clientProcess = {
39
+ claude: "Claude",
40
+ jan: "Jan",
41
+ // Add more clients here
42
+ }[client] || client;
43
+ try {
44
+ const platform = process.platform;
45
+ if (platform === "win32") {
46
+ await execAsync(`taskkill /F /IM "${clientProcess}.exe" && start "" "${clientProcess}.exe"`);
47
+ }
48
+ else if (platform === "darwin") {
49
+ await execAsync(`killall "${clientProcess}" && open -a "${clientProcess}"`);
50
+ }
51
+ else if (platform === "linux") {
52
+ await execAsync(`pkill -f "${clientProcess.toLowerCase()}" && ${clientProcess.toLowerCase()}`);
53
+ }
54
+ // Wait a moment for the app to close before reopening
55
+ await new Promise((resolve) => setTimeout(resolve, 2000));
56
+ // Reopen the app
57
+ if (platform === "win32") {
58
+ await execAsync(`start "" "${clientProcess}.exe"`);
59
+ }
60
+ else if (platform === "darwin") {
61
+ await execAsync(`open -a "${clientProcess}"`);
62
+ }
63
+ else if (platform === "linux") {
64
+ await execAsync(clientProcess.toLowerCase());
65
+ }
66
+ console.log(`${clientProcess} has been restarted.`);
67
+ }
68
+ catch (error) {
69
+ console.error(`Failed to restart ${clientProcess}:`, error);
70
+ }
71
+ }
72
+ export async function promptForRestart(client) {
73
+ if (!client)
74
+ return false;
75
+ // Check if client is running first
76
+ const isRunning = await isClientRunning(client);
77
+ if (!isRunning) {
78
+ return false;
79
+ }
80
+ const { shouldRestart } = await inquirer.prompt([
81
+ {
82
+ type: "confirm",
83
+ name: "shouldRestart",
84
+ message: `Would you like to restart the ${client} app to apply changes?`,
85
+ default: true,
86
+ },
87
+ ]);
88
+ if (shouldRestart) {
89
+ console.log(`Restarting ${client} app...`);
90
+ await restartClient(client);
91
+ }
92
+ return shouldRestart;
93
+ }
@@ -0,0 +1,118 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ // biome-ignore lint/complexity/noStaticOnlyClass: <explanation>
5
+ export class ConfigManager {
6
+ static getConfigPath(client) {
7
+ const normalizedClient = client?.toLowerCase() || "claude";
8
+ return ConfigManager.clientPaths[normalizedClient] || path.join(path.dirname(ConfigManager.configPath), "..", client || "claude", `${normalizedClient}_config.json`);
9
+ }
10
+ static readConfig(client) {
11
+ try {
12
+ const configPath = ConfigManager.getConfigPath(client);
13
+ if (!fs.existsSync(configPath)) {
14
+ return { mcpServers: {} };
15
+ }
16
+ const rawConfig = JSON.parse(fs.readFileSync(configPath, "utf8"));
17
+ return {
18
+ mcpServers: rawConfig.mcpServers || {},
19
+ };
20
+ }
21
+ catch (error) {
22
+ return { mcpServers: {} };
23
+ }
24
+ }
25
+ static writeConfig(config, client) {
26
+ const configPath = ConfigManager.getConfigPath(client);
27
+ const configDir = path.dirname(configPath);
28
+ if (!fs.existsSync(configDir)) {
29
+ fs.mkdirSync(configDir, { recursive: true });
30
+ }
31
+ if (!config.mcpServers || typeof config.mcpServers !== "object") {
32
+ throw new Error("Invalid config structure");
33
+ }
34
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
35
+ }
36
+ static isServerInstalled(id, client) {
37
+ const config = ConfigManager.readConfig(client);
38
+ const normalizedId = ConfigManager.normalizeServerId(id);
39
+ return normalizedId in config.mcpServers;
40
+ }
41
+ static async installServer(id, serverConfig, client) {
42
+ const normalizedId = ConfigManager.normalizeServerId(id);
43
+ const config = ConfigManager.readConfig(client);
44
+ config.mcpServers[normalizedId] = serverConfig;
45
+ ConfigManager.writeConfig(config, client);
46
+ }
47
+ static async uninstallServer(id, client) {
48
+ const normalizedId = ConfigManager.normalizeServerId(id);
49
+ const config = ConfigManager.readConfig(client);
50
+ if (!config.mcpServers[normalizedId]) {
51
+ console.log(`Server ${normalizedId} not found in configuration`);
52
+ return;
53
+ }
54
+ delete config.mcpServers[normalizedId];
55
+ ConfigManager.writeConfig(config, client);
56
+ }
57
+ static getServerConfig(id, client) {
58
+ const config = ConfigManager.readConfig(client);
59
+ return config.mcpServers[id] || null;
60
+ }
61
+ static envVarsToArgs(envVars) {
62
+ return Object.entries(envVars).flatMap(([key, value]) => {
63
+ const argName = key.toLowerCase().replace(/_/g, "-");
64
+ return [`--${argName}`, value];
65
+ });
66
+ }
67
+ static normalizeServerId(serverId) {
68
+ if (serverId.startsWith("@")) {
69
+ const firstSlashIndex = serverId.indexOf("/");
70
+ if (firstSlashIndex !== -1) {
71
+ return `${serverId.substring(0, firstSlashIndex)}-${serverId.substring(firstSlashIndex + 1)}`;
72
+ }
73
+ }
74
+ return serverId;
75
+ }
76
+ static denormalizeServerId(normalizedId) {
77
+ if (normalizedId.startsWith("@")) {
78
+ const dashIndex = normalizedId.indexOf("-");
79
+ if (dashIndex !== -1) {
80
+ return `${normalizedId.substring(0, dashIndex)}/${normalizedId.substring(dashIndex + 1)}`;
81
+ }
82
+ }
83
+ return normalizedId;
84
+ }
85
+ // get locally installed servers
86
+ static getInstalledServerIds(client) {
87
+ const config = ConfigManager.readConfig(client);
88
+ const ids = Object.keys(config.mcpServers || {});
89
+ return ids;
90
+ }
91
+ }
92
+ (() => {
93
+ const homeDir = os.homedir();
94
+ // Define platform-specific base directories
95
+ const platformPaths = {
96
+ win32: {
97
+ baseDir: process.env.APPDATA || path.join(homeDir, "AppData", "Roaming"),
98
+ vscodePath: path.join("Code", "User", "globalStorage")
99
+ },
100
+ darwin: {
101
+ baseDir: path.join(homeDir, "Library", "Application Support"),
102
+ vscodePath: path.join("Code", "User", "globalStorage")
103
+ },
104
+ linux: {
105
+ baseDir: process.env.XDG_CONFIG_HOME || path.join(homeDir, ".config"),
106
+ vscodePath: path.join("Code/User/globalStorage")
107
+ }
108
+ };
109
+ const platform = process.platform;
110
+ const { baseDir, vscodePath } = platformPaths[platform];
111
+ // Define client paths using the platform-specific base directories
112
+ const clientPaths = {
113
+ claude: path.join(baseDir, "Claude", "claude_desktop_config.json"),
114
+ cline: path.join(baseDir, vscodePath, "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json")
115
+ };
116
+ ConfigManager.configPath = clientPaths.claude;
117
+ ConfigManager.clientPaths = clientPaths;
118
+ })();
@@ -0,0 +1,87 @@
1
+ import dotenv from "dotenv";
2
+ import { ConfigManager } from "./config-manager.js";
3
+ dotenv.config();
4
+ export const REGISTRY_ENDPOINT = process.env.REGISTRY_ENDPOINT || "https://registry.smithery.ai";
5
+ export async function fetchServers(client, serverIds = []) {
6
+ try {
7
+ if (serverIds.length === 0) {
8
+ return [];
9
+ }
10
+ // Fetch all servers in parallel
11
+ const serverPromises = serverIds.map((id) => resolveServer(id, client));
12
+ const resolvedServers = await Promise.all(serverPromises);
13
+ // Filter out null results
14
+ return resolvedServers.filter((server) => server !== null);
15
+ }
16
+ catch (error) {
17
+ throw new Error(`Failed to resolve servers: ${error instanceof Error ? error.message : String(error)}`);
18
+ }
19
+ }
20
+ // Resolves a single servers by ID from registry or local installation
21
+ // Returns null if server cannot be found in either location
22
+ export async function resolveServer(serverId, client) {
23
+ try {
24
+ // Check if server is installed first
25
+ // const config = ConfigManager.readConfig()
26
+ const isInstalled = ConfigManager.isServerInstalled(serverId, client);
27
+ const response = await fetch(`${REGISTRY_ENDPOINT}/servers/${serverId}`);
28
+ if (!response.ok) {
29
+ // If server is installed but not in registry, return basic info
30
+ if (isInstalled) {
31
+ return {
32
+ id: serverId,
33
+ name: serverId,
34
+ connections: [],
35
+ isInstalled: true,
36
+ client: client,
37
+ };
38
+ }
39
+ return null;
40
+ }
41
+ const registryServer = await response.json();
42
+ const resolvedServer = {
43
+ id: registryServer.id,
44
+ name: registryServer.name,
45
+ connections: registryServer.connections,
46
+ isInstalled: isInstalled, // Use the checked installation status
47
+ client: client,
48
+ };
49
+ return resolvedServer;
50
+ }
51
+ catch (error) {
52
+ return null;
53
+ }
54
+ }
55
+ export async function getServerConfiguration(serverId, configValues, connectionType = "stdio") {
56
+ try {
57
+ const requestBody = {
58
+ connectionType,
59
+ config: configValues,
60
+ };
61
+ const response = await fetch(`${REGISTRY_ENDPOINT}/servers/${serverId}`, {
62
+ method: "POST",
63
+ headers: {
64
+ "Content-Type": "application/json",
65
+ },
66
+ body: JSON.stringify(requestBody),
67
+ });
68
+ const text = await response.text();
69
+ if (!text) {
70
+ throw new Error("Empty response received from registry");
71
+ }
72
+ try {
73
+ const parsed = JSON.parse(text);
74
+ return parsed.result;
75
+ }
76
+ catch (parseError) {
77
+ const errorMessage = parseError instanceof Error
78
+ ? parseError.message
79
+ : "Unknown parsing error";
80
+ throw new Error(`Invalid JSON response from registry: ${errorMessage}`);
81
+ }
82
+ }
83
+ catch (error) {
84
+ console.error("Error getting server configuration:", error);
85
+ return null;
86
+ }
87
+ }
@@ -0,0 +1,141 @@
1
+ import { exec } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import inquirer from "inquirer";
4
+ import chalk from "chalk";
5
+ const execAsync = promisify(exec);
6
+ export async function checkUVInstalled() {
7
+ try {
8
+ await execAsync("uvx --version");
9
+ return true;
10
+ }
11
+ catch (error) {
12
+ return false;
13
+ }
14
+ }
15
+ export async function promptForUVInstall(inquirerInstance) {
16
+ const { shouldInstall } = await inquirerInstance.prompt([
17
+ {
18
+ type: "confirm",
19
+ name: "shouldInstall",
20
+ message: "UV package manager is required for Python MCP servers. Would you like to install it?",
21
+ default: true,
22
+ },
23
+ ]);
24
+ if (!shouldInstall) {
25
+ console.warn(chalk.yellow("UV installation was declined. You can install it manually from https://astral.sh/uv"));
26
+ return false;
27
+ }
28
+ console.log("Installing uv package manager...");
29
+ try {
30
+ await execAsync("curl -LsSf https://astral.sh/uv/install.sh | sh");
31
+ console.log(chalk.green("✓ UV installed successfully"));
32
+ return true;
33
+ }
34
+ catch (error) {
35
+ console.warn(chalk.yellow("Failed to install UV. You can install it manually from https://astral.sh/uv"));
36
+ return false;
37
+ }
38
+ }
39
+ export async function collectConfigValues(connection) {
40
+ const promptsMap = new Map();
41
+ // Process config schema if it exists
42
+ if (connection.configSchema?.properties) {
43
+ const required = new Set(connection.configSchema.required || []);
44
+ Object.entries(connection.configSchema.properties).forEach(([key, prop]) => {
45
+ const schemaProp = prop;
46
+ promptsMap.set(key, {
47
+ key,
48
+ description: schemaProp.description || `Enter value for ${key}`,
49
+ required: required.has(key),
50
+ default: schemaProp.default,
51
+ type: schemaProp.type,
52
+ });
53
+ });
54
+ }
55
+ const configValues = {};
56
+ function convertValueToType(value, type) {
57
+ if (!type)
58
+ return value;
59
+ // Handle empty string inputs
60
+ if (value === "") {
61
+ switch (type) {
62
+ case "array":
63
+ return [];
64
+ case "number":
65
+ case "integer":
66
+ return null;
67
+ default:
68
+ return value;
69
+ }
70
+ }
71
+ if (!value)
72
+ return value;
73
+ switch (type) {
74
+ case "boolean":
75
+ return String(value).toLowerCase() === "true";
76
+ case "number":
77
+ return Number(value);
78
+ case "integer":
79
+ return Number.parseInt(String(value), 10);
80
+ case "array":
81
+ // Parse comma-separated string into array
82
+ return String(value)
83
+ .split(",")
84
+ .map((item) => item.trim())
85
+ .filter((item) => item !== ""); // Remove empty items
86
+ default:
87
+ return value;
88
+ }
89
+ }
90
+ // Iterate through each configuration prompt
91
+ for (const prompt of promptsMap.values()) {
92
+ // If env var exists and setting is optional, ask if user wants to reuse it
93
+ if (process.env[prompt.key] && !prompt.required) {
94
+ const { reuseExisting } = await inquirer.prompt([
95
+ {
96
+ type: "confirm",
97
+ name: "reuseExisting",
98
+ message: `Found ${prompt.key} in environment. Use it?`,
99
+ default: true,
100
+ },
101
+ ]);
102
+ if (reuseExisting) {
103
+ configValues[prompt.key] = convertValueToType(process.env[prompt.key], prompt.type);
104
+ continue;
105
+ }
106
+ }
107
+ const requiredText = prompt.required
108
+ ? chalk.red(" (required)")
109
+ : chalk.gray(" (optional)");
110
+ const promptType = prompt.key.toLowerCase().includes("key")
111
+ ? "password" // Use password type for any field containing 'key'
112
+ : prompt.type === "boolean"
113
+ ? "confirm"
114
+ : prompt.type === "array"
115
+ ? "input"
116
+ : prompt.type === "number" || prompt.type === "integer"
117
+ ? "number"
118
+ : "input";
119
+ const { value } = await inquirer.prompt([
120
+ {
121
+ type: promptType,
122
+ name: "value",
123
+ message: `${prompt.description}${requiredText}${prompt.type === "array" ? " (comma-separated)" : ""}`,
124
+ default: prompt.default,
125
+ mask: promptType === "password" ? "*" : undefined, // Add masking for password fields
126
+ validate: (input) => {
127
+ if (prompt.required && !input)
128
+ return false;
129
+ if (prompt.type === "number" || prompt.type === "integer") {
130
+ return !Number.isNaN(Number(input)) || "Please enter a valid number";
131
+ }
132
+ return true;
133
+ },
134
+ },
135
+ ]);
136
+ if (value !== undefined || prompt.default !== undefined) {
137
+ configValues[prompt.key] = convertValueToType(value ?? prompt.default, prompt.type);
138
+ }
139
+ }
140
+ return configValues;
141
+ }
@@ -0,0 +1,43 @@
1
+ import chalk from "chalk";
2
+ import { ServerManager } from "./server-manager.js";
3
+ import { displayServerDetails, confirmUninstall } from "./server-display.js";
4
+ const serverManager = new ServerManager();
5
+ export async function handleServerAction(server, action, handlers, showActionsAfter = true, client = "claude") {
6
+ switch (action) {
7
+ case "install":
8
+ console.log(chalk.cyan(`\nPreparing to install ${server.name}...`));
9
+ await serverManager.installServer(server, client);
10
+ server.isInstalled = true;
11
+ if (handlers.onInstall) {
12
+ await handlers.onInstall(server);
13
+ }
14
+ console.log(chalk.green(`\nSuccessfully installed ${server.name}`));
15
+ return; // Exit after successful installation
16
+ case "uninstall":
17
+ if (await confirmUninstall(server.name)) {
18
+ await serverManager.uninstallServer(server.id, client);
19
+ console.log(chalk.green(`Successfully uninstalled ${server.name}`));
20
+ server.isInstalled = false;
21
+ if (handlers.onUninstall) {
22
+ await handlers.onUninstall(server);
23
+ }
24
+ return; // Exit after successful uninstallation
25
+ }
26
+ else {
27
+ console.log("Uninstallation cancelled.");
28
+ }
29
+ break;
30
+ case "back":
31
+ if (handlers.onBack) {
32
+ await handlers.onBack();
33
+ }
34
+ return;
35
+ case "exit":
36
+ process.exit(0);
37
+ }
38
+ // Show actions again after completing an action (except for exit/back)
39
+ if (showActionsAfter) {
40
+ const nextAction = await displayServerDetails(server);
41
+ await handleServerAction(server, nextAction, handlers, showActionsAfter, client);
42
+ }
43
+ }
@@ -0,0 +1,80 @@
1
+ import chalk from "chalk";
2
+ import inquirer from "inquirer";
3
+ export function formatServerChoice(server, showInstallStatus = false) {
4
+ const prefix = showInstallStatus ? (server.isInstalled ? "✓ " : " ") : "";
5
+ return {
6
+ name: `${prefix}${server.name} | ${server.id}`,
7
+ value: server,
8
+ short: server.name,
9
+ };
10
+ }
11
+ export function createListChoices(servers, includeBack = true, includeExit = true) {
12
+ const choices = servers.map((server) => formatServerChoice(server, true));
13
+ if (includeBack || includeExit) {
14
+ choices.push(new inquirer.Separator());
15
+ }
16
+ if (includeBack) {
17
+ choices.push({
18
+ name: chalk.yellow("↩ Back"),
19
+ value: "back",
20
+ short: "Back",
21
+ });
22
+ }
23
+ if (includeExit) {
24
+ choices.push({
25
+ name: chalk.red("✖ Exit"),
26
+ value: "exit",
27
+ short: "Exit",
28
+ });
29
+ }
30
+ return choices;
31
+ }
32
+ export function printServerListHeader(count, type = "all") {
33
+ console.log(chalk.bold.cyan(`\n📦 ${type === "installed" ? "Installed Servers" : "Available Servers"}`));
34
+ console.log(chalk.gray(`Found ${count} ${type === "installed" ? "installed " : ""}servers\n`));
35
+ }
36
+ export async function displayServerDetails(server, includeBack = true) {
37
+ console.log(`\n${chalk.bold.cyan("Server Details:")}`);
38
+ console.log(chalk.bold("ID: ") + server.id);
39
+ console.log(chalk.bold("Name: ") + server.name);
40
+ const choices = [
41
+ {
42
+ name: server.isInstalled
43
+ ? chalk.yellow("🔄 Reinstall this server")
44
+ : chalk.yellow("📦 Install this server"),
45
+ value: "install",
46
+ },
47
+ ...(server.isInstalled
48
+ ? [
49
+ {
50
+ name: chalk.yellow("🗑️ Uninstall this server"),
51
+ value: "uninstall",
52
+ },
53
+ ]
54
+ : []),
55
+ ...(includeBack
56
+ ? [{ name: chalk.yellow("↩ Back to list"), value: "back" }]
57
+ : []),
58
+ { name: chalk.red("✖ Exit"), value: "exit" },
59
+ ];
60
+ const { action } = await inquirer.prompt([
61
+ {
62
+ type: "list",
63
+ name: "action",
64
+ message: "What would you like to do?",
65
+ choices,
66
+ },
67
+ ]);
68
+ return action;
69
+ }
70
+ export async function confirmUninstall(serverName) {
71
+ const { confirm } = await inquirer.prompt([
72
+ {
73
+ type: "confirm",
74
+ name: "confirm",
75
+ message: `Are you sure you want to uninstall ${serverName}?`,
76
+ default: false,
77
+ },
78
+ ]);
79
+ return confirm;
80
+ }