@smithery/cli 0.0.3 → 0.0.9

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.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # smithery/get
1
+ # smithery/cli
2
2
 
3
3
  A lightweight registry and package manager for Model Context Protocol (MCP) servers, designed to be client-agnostic.
4
4
 
@@ -35,10 +35,11 @@ npx .
35
35
 
36
36
  ### Common Commands
37
37
 
38
- Note: yet to fix 'npx . list' to list all packages
39
-
40
38
  Test specific functionality:
41
39
  ```bash
40
+ # List all packages
41
+ npx . list
42
+
42
43
  # Get details about a specific package
43
44
  npx . get <package-id>
44
45
 
@@ -2,6 +2,7 @@ import { installPackage as installPkg } from "../utils/package-management.js";
2
2
  import inquirer from "inquirer";
3
3
  import chalk from "chalk";
4
4
  import { resolvePackage } from "../utils/package-resolver.js";
5
+ import { VALID_CLIENTS } from "../utils/config.js";
5
6
  async function promptForRuntime() {
6
7
  const { runtime } = await inquirer.prompt([
7
8
  {
@@ -16,7 +17,7 @@ async function promptForRuntime() {
16
17
  ]);
17
18
  return runtime;
18
19
  }
19
- function createUnknownPackage(id, runtime) {
20
+ function createUnknownPackage(id, runtime, client) {
20
21
  return {
21
22
  id,
22
23
  name: id,
@@ -28,6 +29,7 @@ function createUnknownPackage(id, runtime) {
28
29
  license: "",
29
30
  isInstalled: false,
30
31
  isVerified: false,
32
+ client: client,
31
33
  connections: [
32
34
  {
33
35
  stdio: {
@@ -42,10 +44,14 @@ function createUnknownPackage(id, runtime) {
42
44
  export async function installPackage(pkg) {
43
45
  return installPkg(pkg);
44
46
  }
45
- export async function install(packageId) {
46
- const pkg = await resolvePackage(packageId);
47
+ export async function install(packageId, client) {
48
+ if (client && !VALID_CLIENTS.includes(client)) {
49
+ console.error(chalk.red(`Invalid client: ${client}\nValid clients are: ${VALID_CLIENTS.join(", ")}`));
50
+ process.exit(1);
51
+ }
52
+ const pkg = await resolvePackage(packageId, client);
47
53
  if (!pkg) {
48
- console.warn(chalk.yellow(`Package ${packageId} not found in the curated list.`));
54
+ console.warn(chalk.yellow(`Package ${packageId} not found in registry.`));
49
55
  const { proceedWithInstall } = await inquirer.prompt([
50
56
  {
51
57
  type: "confirm",
@@ -57,7 +63,7 @@ export async function install(packageId) {
57
63
  if (proceedWithInstall) {
58
64
  console.log(chalk.cyan(`Proceeding with installation of ${packageId}...`));
59
65
  const runtime = await promptForRuntime();
60
- const unknownPkg = createUnknownPackage(packageId, runtime);
66
+ const unknownPkg = createUnknownPackage(packageId, runtime, client);
61
67
  await installPkg(unknownPkg);
62
68
  }
63
69
  else {
package/dist/index.js CHANGED
@@ -6,6 +6,8 @@ import { listInstalledPackages } from "./commands/installed.js";
6
6
  import { get } from "./commands/get.js";
7
7
  const command = process.argv[2];
8
8
  const packageName = process.argv[3];
9
+ const clientFlag = process.argv.indexOf("--client");
10
+ const client = clientFlag !== -1 ? process.argv[clientFlag + 1] : undefined;
9
11
  async function main() {
10
12
  switch (command) {
11
13
  case "list":
@@ -16,7 +18,7 @@ async function main() {
16
18
  console.error("Please provide a package name to install");
17
19
  process.exit(1);
18
20
  }
19
- await install(packageName);
21
+ await install(packageName, client);
20
22
  break;
21
23
  case "uninstall":
22
24
  await uninstall(packageName);
@@ -35,6 +37,7 @@ async function main() {
35
37
  console.log("Available commands:");
36
38
  console.log(" list List all available packages");
37
39
  console.log(" install <package> Install a package");
40
+ console.log(" --client <name> Specify the AI client");
38
41
  console.log(" uninstall [package] Uninstall a package");
39
42
  console.log(" installed List installed packages");
40
43
  console.log(" get <package> Get details for a specific package");
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,44 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { ConfigManager } from "./config-manager.js";
3
+ import { REGISTRY_ENDPOINT } from "./package-resolver.js";
4
+ export class Analytics {
5
+ constructor() {
6
+ this.sessionId = randomUUID();
7
+ const prefs = ConfigManager.readPreferences();
8
+ if (!prefs.anonUserId) {
9
+ ConfigManager.writePreferences({ ...prefs, anonUserId: randomUUID() });
10
+ }
11
+ }
12
+ async trackInstallation(packageId, clientType) {
13
+ const prefs = ConfigManager.readPreferences();
14
+ try {
15
+ const event = {
16
+ anonUserId: prefs.anonUserId,
17
+ eventType: "package_installation",
18
+ packageId: packageId,
19
+ timestamp: new Date().toISOString(),
20
+ platform: process.platform,
21
+ nodeVersion: process.version,
22
+ sessionId: this.sessionId,
23
+ clientType,
24
+ };
25
+ await this.sendEvent(event);
26
+ }
27
+ catch (error) {
28
+ console.error("Failed to track installation:", error);
29
+ }
30
+ }
31
+ async sendEvent(event) {
32
+ const response = await fetch(`${REGISTRY_ENDPOINT}/install`, {
33
+ method: "POST",
34
+ headers: {
35
+ "Content-Type": "application/json",
36
+ },
37
+ body: JSON.stringify(event),
38
+ });
39
+ if (!response.ok) {
40
+ throw new Error(`Analytics request failed: ${response.statusText}`);
41
+ }
42
+ }
43
+ }
44
+ export const analytics = new Analytics();
@@ -1,17 +1,26 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import os from "node:os";
4
+ // biome-ignore lint/complexity/noStaticOnlyClass: <explanation>
4
5
  export class ConfigManager {
5
- static getConfigPath() {
6
- return ConfigManager.configPath;
6
+ static getConfigPath(client) {
7
+ const baseDir = path.dirname(ConfigManager.configPath);
8
+ // Default to Claude if no client specified or if explicitly Claude
9
+ if (!client || client.toLowerCase() === "claude") {
10
+ return ConfigManager.configPath;
11
+ }
12
+ // Support for future AI clients
13
+ // Currently only Claude is officially supported
14
+ // 'jan' and others are placeholders for future integrations
15
+ return path.join(baseDir, "..", client, `${client.toLowerCase()}_config.json`);
7
16
  }
8
- static readConfig() {
17
+ static readConfig(client) {
9
18
  try {
10
- if (!fs.existsSync(ConfigManager.configPath)) {
19
+ const configPath = ConfigManager.getConfigPath(client);
20
+ if (!fs.existsSync(configPath)) {
11
21
  return { mcpServers: {} };
12
22
  }
13
- const rawConfig = JSON.parse(fs.readFileSync(ConfigManager.configPath, "utf8"));
14
- // Ensure we have the correct structure
23
+ const rawConfig = JSON.parse(fs.readFileSync(configPath, "utf8"));
15
24
  return {
16
25
  mcpServers: rawConfig.mcpServers || {},
17
26
  };
@@ -21,17 +30,17 @@ export class ConfigManager {
21
30
  return { mcpServers: {} };
22
31
  }
23
32
  }
24
- static writeConfig(config) {
33
+ static writeConfig(config, client) {
25
34
  try {
26
- const configDir = path.dirname(ConfigManager.configPath);
35
+ const configPath = ConfigManager.getConfigPath(client);
36
+ const configDir = path.dirname(configPath);
27
37
  if (!fs.existsSync(configDir)) {
28
38
  fs.mkdirSync(configDir, { recursive: true });
29
39
  }
30
- // Validate config structure before writing
31
40
  if (!config.mcpServers || typeof config.mcpServers !== "object") {
32
41
  throw new Error("Invalid config structure");
33
42
  }
34
- fs.writeFileSync(ConfigManager.configPath, JSON.stringify(config, null, 2));
43
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
35
44
  }
36
45
  catch (error) {
37
46
  console.error("Error writing config:", error);
@@ -62,32 +71,32 @@ export class ConfigManager {
62
71
  throw error;
63
72
  }
64
73
  }
65
- static isPackageInstalled(id) {
66
- const config = ConfigManager.readConfig();
74
+ static isPackageInstalled(id, client) {
75
+ const config = ConfigManager.readConfig(client);
67
76
  return id in config.mcpServers;
68
77
  }
69
- static async installPackage(id, serverConfig) {
78
+ static async installPackage(id, serverConfig, client) {
70
79
  try {
71
- const config = ConfigManager.readConfig();
80
+ const config = ConfigManager.readConfig(client);
72
81
  config.mcpServers[id] = serverConfig;
73
- ConfigManager.writeConfig(config);
82
+ ConfigManager.writeConfig(config, client);
74
83
  }
75
84
  catch (error) {
76
85
  console.error("Error installing server:", error);
77
86
  throw error;
78
87
  }
79
88
  }
80
- static async uninstallPackage(id) {
81
- const config = ConfigManager.readConfig();
89
+ static async uninstallPackage(id, client) {
90
+ const config = ConfigManager.readConfig(client);
82
91
  if (!config.mcpServers[id]) {
83
92
  console.log(`Server ${id} is not installed.`);
84
93
  return;
85
94
  }
86
95
  delete config.mcpServers[id];
87
- ConfigManager.writeConfig(config);
96
+ ConfigManager.writeConfig(config, client);
88
97
  }
89
- static getServerConfig(id) {
90
- const config = ConfigManager.readConfig();
98
+ static getServerConfig(id, client) {
99
+ const config = ConfigManager.readConfig(client);
91
100
  return config.mcpServers[id] || null;
92
101
  }
93
102
  }
@@ -8,6 +8,7 @@ import { promisify } from "node:util";
8
8
  const __filename = fileURLToPath(import.meta.url);
9
9
  const __dirname = dirname(__filename);
10
10
  const execAsync = promisify(exec);
11
+ export const VALID_CLIENTS = ["claude"];
11
12
  function getPackageRuntime(packageName) {
12
13
  const packageListPath = path.join(dirname(__dirname), "../packages/package-list.json");
13
14
  if (!fs.existsSync(packageListPath)) {
@@ -2,6 +2,7 @@ import chalk from "chalk";
2
2
  import inquirer from "inquirer";
3
3
  export async function displayPackageDetailsWithActions(pkg) {
4
4
  console.log(`\n${chalk.bold.cyan("Package Details:")}`);
5
+ console.log(chalk.bold("ID: ") + pkg.id);
5
6
  console.log(chalk.bold("Name: ") + pkg.name);
6
7
  console.log(chalk.bold("Description: ") + pkg.description);
7
8
  console.log(chalk.bold("Vendor: ") + pkg.vendor);
@@ -2,6 +2,7 @@ import { exec } from "node:child_process";
2
2
  import inquirer from "inquirer";
3
3
  import { promisify } from "node:util";
4
4
  import { ConfigManager } from "./config-manager.js";
5
+ import { analytics } from "./analytics.js";
5
6
  const execAsync = promisify(exec);
6
7
  async function checkAnalyticsConsent() {
7
8
  const prefs = ConfigManager.readPreferences();
@@ -19,9 +20,8 @@ async function checkAnalyticsConsent() {
19
20
  ConfigManager.writePreferences({ ...prefs, allowAnalytics });
20
21
  return allowAnalytics;
21
22
  }
22
- async function trackInstallation(packageName) {
23
- // Analytics tracking disabled for now
24
- return;
23
+ async function trackInstallation(packageName, client) {
24
+ return analytics.trackInstallation(packageName, client);
25
25
  }
26
26
  async function promptForPlaceholder(key, message) {
27
27
  const { value } = await inquirer.prompt([
@@ -80,72 +80,92 @@ async function promptForEnvVars(pkg) {
80
80
  }
81
81
  return Object.keys(envVars).length > 0 ? envVars : undefined;
82
82
  }
83
- async function isClaudeRunning() {
83
+ async function isClientRunning(client) {
84
+ if (!client)
85
+ return false;
84
86
  try {
85
87
  const platform = process.platform;
88
+ // Map of supported clients to their process names
89
+ // Currently only Claude is officially supported
90
+ // Other entries are placeholders for future integrations
91
+ const clientProcess = {
92
+ claude: "Claude",
93
+ // jan: "Jan", // Placeholder for future client integration
94
+ // Add more clients here as they become supported
95
+ }[client] || client;
86
96
  if (platform === "win32") {
87
- const { stdout } = await execAsync('tasklist /FI "IMAGENAME eq Claude.exe" /NH');
88
- return stdout.includes("Claude.exe");
97
+ const { stdout } = await execAsync(`tasklist /FI "IMAGENAME eq ${clientProcess}.exe" /NH`);
98
+ return stdout.includes(`${clientProcess}.exe`);
89
99
  }
90
100
  else if (platform === "darwin") {
91
- const { stdout } = await execAsync('pgrep -x "Claude"');
101
+ const { stdout } = await execAsync(`pgrep -x "${clientProcess}"`);
92
102
  return !!stdout.trim();
93
103
  }
94
104
  else if (platform === "linux") {
95
- const { stdout } = await execAsync('pgrep -f "claude"');
105
+ const { stdout } = await execAsync(`pgrep -f "${clientProcess.toLowerCase()}"`);
96
106
  return !!stdout.trim();
97
107
  }
98
108
  return false;
99
109
  }
100
110
  catch (error) {
101
- // If the command fails, assume Claude is not running
111
+ // If the command fails, assume client is not running
102
112
  return false;
103
113
  }
104
114
  }
105
- async function promptForRestart() {
106
- // Check if Claude is running first
107
- const claudeRunning = await isClaudeRunning();
108
- if (!claudeRunning) {
115
+ async function restartClient(client) {
116
+ const clientProcess = {
117
+ claude: "Claude",
118
+ jan: "Jan",
119
+ // Add more clients here
120
+ }[client] || client;
121
+ try {
122
+ const platform = process.platform;
123
+ if (platform === "win32") {
124
+ await execAsync(`taskkill /F /IM "${clientProcess}.exe" && start "" "${clientProcess}.exe"`);
125
+ }
126
+ else if (platform === "darwin") {
127
+ await execAsync(`killall "${clientProcess}" && open -a "${clientProcess}"`);
128
+ }
129
+ else if (platform === "linux") {
130
+ await execAsync(`pkill -f "${clientProcess.toLowerCase()}" && ${clientProcess.toLowerCase()}`);
131
+ }
132
+ // Wait a moment for the app to close before reopening
133
+ await new Promise((resolve) => setTimeout(resolve, 2000));
134
+ // Reopen the app
135
+ if (platform === "win32") {
136
+ await execAsync(`start "" "${clientProcess}.exe"`);
137
+ }
138
+ else if (platform === "darwin") {
139
+ await execAsync(`open -a "${clientProcess}"`);
140
+ }
141
+ else if (platform === "linux") {
142
+ await execAsync(clientProcess.toLowerCase());
143
+ }
144
+ console.log(`${clientProcess} has been restarted.`);
145
+ }
146
+ catch (error) {
147
+ console.error(`Failed to restart ${clientProcess}:`, error);
148
+ }
149
+ }
150
+ async function promptForRestart(client) {
151
+ if (!client)
152
+ return false;
153
+ // Check if client is running first
154
+ const isRunning = await isClientRunning(client);
155
+ if (!isRunning) {
109
156
  return false;
110
157
  }
111
158
  const { shouldRestart } = await inquirer.prompt([
112
159
  {
113
160
  type: "confirm",
114
161
  name: "shouldRestart",
115
- message: "Would you like to restart the Claude desktop app to apply changes?",
162
+ message: `Would you like to restart the ${client} app to apply changes?`,
116
163
  default: true,
117
164
  },
118
165
  ]);
119
166
  if (shouldRestart) {
120
- console.log("Restarting Claude desktop app...");
121
- try {
122
- const platform = process.platform;
123
- if (platform === "win32") {
124
- await execAsync('taskkill /F /IM "Claude.exe" && start "" "Claude.exe"');
125
- }
126
- else if (platform === "darwin") {
127
- await execAsync('killall "Claude" && open -a "Claude"');
128
- }
129
- else if (platform === "linux") {
130
- await execAsync('pkill -f "claude" && claude');
131
- }
132
- // Wait a moment for the app to close before reopening
133
- await new Promise((resolve) => setTimeout(resolve, 2000));
134
- // Reopen the app
135
- if (platform === "win32") {
136
- await execAsync('start "" "Claude.exe"');
137
- }
138
- else if (platform === "darwin") {
139
- await execAsync('open -a "Claude"');
140
- }
141
- else if (platform === "linux") {
142
- await execAsync("claude");
143
- }
144
- console.log("Claude desktop app has been restarted.");
145
- }
146
- catch (error) {
147
- console.error("Failed to restart Claude desktop app:", error);
148
- }
167
+ console.log(`Restarting ${client} app...`);
168
+ await restartClient(client);
149
169
  }
150
170
  return shouldRestart;
151
171
  }
@@ -163,32 +183,32 @@ export async function installPackage(pkg) {
163
183
  } */
164
184
  const envVars = await promptForEnvVars(pkg);
165
185
  // Use the connection stdio config directly
166
- const connection = pkg.connections?.[0]; // Take first connection
186
+ const connection = pkg.connections?.[0];
167
187
  const serverConfig = {
168
188
  command: connection.stdio.command,
169
189
  args: connection.stdio.args,
170
- env: envVars || {}, // Use our prompted env vars
171
- // runtime: pkg.runtime
190
+ env: {
191
+ ...(envVars || {}),
192
+ },
172
193
  };
173
- await ConfigManager.installPackage(pkg.id, serverConfig);
174
- console.log("Updated Claude desktop configuration");
175
- // Check analytics consent and track if allowed
176
- const analyticsAllowed = await checkAnalyticsConsent();
177
- if (analyticsAllowed) {
178
- await trackInstallation(pkg.name);
194
+ // Install package with client-specific configuration
195
+ await ConfigManager.installPackage(pkg.id, serverConfig, pkg.client);
196
+ console.log(`Updated ${pkg.client || "default"} configuration`);
197
+ if (await checkAnalyticsConsent()) {
198
+ await trackInstallation(pkg.id, pkg.client);
179
199
  }
180
- await promptForRestart();
200
+ await promptForRestart(pkg.client);
181
201
  }
182
202
  catch (error) {
183
203
  console.error("Failed to install package:", error);
184
204
  throw error;
185
205
  }
186
206
  }
187
- export async function uninstallPackage(packageName) {
207
+ export async function uninstallPackage(packageName, client) {
188
208
  try {
189
209
  await ConfigManager.uninstallPackage(packageName);
190
210
  console.log(`\nUninstalled ${packageName}`);
191
- await promptForRestart();
211
+ await promptForRestart(client);
192
212
  }
193
213
  catch (error) {
194
214
  console.error("Failed to uninstall package:", error);
@@ -3,13 +3,16 @@ import { ConfigManager } from "./config-manager.js";
3
3
  // import fs from 'fs';
4
4
  // import { dirname } from 'path';
5
5
  // import { fileURLToPath } from 'url';
6
+ import dotenv from "dotenv";
7
+ dotenv.config();
8
+ export const REGISTRY_ENDPOINT = process.env.REGISTRY_ENDPOINT || "https://registry.smithery.ai";
6
9
  export function isPackageInstalled(packageName) {
7
10
  return ConfigManager.isPackageInstalled(packageName);
8
11
  }
9
- export async function resolvePackages() {
12
+ export async function resolvePackages(client) {
10
13
  try {
11
- // Fetch all packages from registry
12
- const response = await fetch("https://registry.smithery.ai/packages");
14
+ // Updated API endpoint
15
+ const response = await fetch(`${REGISTRY_ENDPOINT}/servers/`);
13
16
  if (!response.ok) {
14
17
  throw new Error(`Failed to fetch packages: ${response.statusText} (${response.status})`);
15
18
  }
@@ -28,64 +31,51 @@ export async function resolvePackages() {
28
31
  : "Unknown parsing error";
29
32
  throw new Error(`Invalid JSON response from registry: ${errorMessage}`);
30
33
  }
31
- const config = ConfigManager.readConfig();
32
- const installedServers = config.mcpServers || {};
33
- const installedIds = Object.keys(installedServers);
34
- // Create a map using IDs
35
- const packageMap = new Map();
36
- for (const pkg of packages) {
37
- packageMap.set(pkg.id, pkg);
38
- }
39
34
  const resolvedPackages = new Map();
40
35
  // Add packages from registry
41
36
  for (const pkg of packages) {
42
37
  resolvedPackages.set(pkg.id, {
43
- id: pkg.id,
44
- name: pkg.name,
45
- description: pkg.description,
46
- vendor: pkg.vendor,
47
- sourceUrl: pkg.sourceUrl,
48
- license: pkg.license,
49
- connections: pkg.connections,
50
- homepage: pkg.homepage,
51
- // runtime: 'node',
38
+ id: pkg.id || "",
39
+ name: pkg.name || "",
40
+ description: pkg.description || "",
41
+ vendor: pkg.vendor || "",
42
+ sourceUrl: pkg.sourceUrl || "",
43
+ license: pkg.license || "",
44
+ connections: pkg.connections || [],
45
+ homepage: pkg.homepage || "",
52
46
  isInstalled: false,
53
47
  isVerified: true,
48
+ client: client,
54
49
  });
55
50
  }
56
51
  // Process installed packages
52
+ const config = ConfigManager.readConfig();
53
+ const installedIds = Object.keys(config.mcpServers || {});
54
+ const installedServers = config.mcpServers || {};
57
55
  for (const id of installedIds) {
58
56
  const installedServer = installedServers[id];
59
- const existingPkg = packageMap.get(id);
57
+ const existingPkg = resolvedPackages.get(id);
60
58
  if (existingPkg) {
61
59
  resolvedPackages.set(id, {
62
- id: existingPkg.id,
63
- name: existingPkg.name,
64
- description: existingPkg.description,
65
- vendor: existingPkg.vendor,
66
- sourceUrl: existingPkg.sourceUrl,
67
- license: existingPkg.license,
68
- connections: existingPkg.connections,
69
- homepage: existingPkg.homepage,
70
- // runtime: installedServer?.runtime || 'node',
60
+ ...existingPkg,
71
61
  isInstalled: true,
72
- isVerified: true,
62
+ client: client || existingPkg.client,
73
63
  });
74
64
  }
75
65
  else {
76
66
  resolvedPackages.set(id, {
77
67
  ...installedServer,
78
- id,
79
- name: id,
68
+ id: id || "",
69
+ name: id || "",
80
70
  description: "Local package",
81
71
  vendor: "Local",
82
72
  sourceUrl: "",
83
73
  license: "",
84
74
  connections: [],
85
75
  homepage: "",
86
- // runtime: installedServer?.runtime || 'node',
87
76
  isInstalled: true,
88
77
  isVerified: false,
78
+ client: client,
89
79
  });
90
80
  }
91
81
  }
@@ -95,10 +85,10 @@ export async function resolvePackages() {
95
85
  throw new Error(`Failed to resolve packages: ${error instanceof Error ? error.message : String(error)}`);
96
86
  }
97
87
  }
98
- export async function resolvePackage(packageId) {
88
+ export async function resolvePackage(packageId, client) {
99
89
  try {
100
- // Replace file reading with registry fetch
101
- const response = await fetch(`https://registry.smithery.ai/${packageId}`);
90
+ // Updated API endpoint
91
+ const response = await fetch(`${REGISTRY_ENDPOINT}/servers/${packageId}`);
102
92
  if (!response.ok) {
103
93
  // Check if it's an installed package
104
94
  const config = ConfigManager.readConfig();
@@ -113,9 +103,9 @@ export async function resolvePackage(packageId) {
113
103
  license: "",
114
104
  connections: [],
115
105
  homepage: "",
116
- // runtime: installedServer.runtime || 'node',
117
106
  isInstalled: true,
118
107
  isVerified: false,
108
+ client: client,
119
109
  };
120
110
  }
121
111
  return null;
@@ -130,9 +120,9 @@ export async function resolvePackage(packageId) {
130
120
  license: registryPackage.license,
131
121
  connections: registryPackage.connections,
132
122
  homepage: registryPackage.homepage,
133
- // runtime: 'node',
134
123
  isInstalled: isPackageInstalled(registryPackage.id),
135
124
  isVerified: true,
125
+ client: client,
136
126
  };
137
127
  return resolvedPackage;
138
128
  }
package/dist/utils/ui.js CHANGED
@@ -3,12 +3,31 @@ import fuzzy from "fuzzy";
3
3
  import chalk from "chalk";
4
4
  export function formatPackageChoice(pkg, showInstallStatus = false) {
5
5
  const prefix = showInstallStatus ? (pkg.isInstalled ? "✓ " : " ") : "";
6
+ const name = (pkg.name || "").toString();
7
+ const description = (pkg.description || "").toString();
8
+ const vendor = (pkg.vendor || "").toString();
9
+ const id = (pkg.id || "").toString();
10
+ // Adjust column widths (removed license column)
11
+ const nameWidth = 20;
12
+ const idWidth = 15;
13
+ const descWidth = 45; // Increased description width
14
+ const vendorWidth = 15;
15
+ // Truncation function remains the same
16
+ const truncateText = (text, width) => {
17
+ if (text.length <= width) {
18
+ return text.padEnd(width);
19
+ }
20
+ return `${text.slice(0, width - 3).padEnd(width - 3)}...`;
21
+ };
22
+ // Apply truncation (removed license)
23
+ const truncatedName = truncateText(name, nameWidth);
24
+ const truncatedId = truncateText(id, idWidth);
25
+ const truncatedDesc = truncateText(description, descWidth);
26
+ const truncatedVendor = truncateText(vendor, vendorWidth);
6
27
  return {
7
- name: `${prefix}${pkg.name.padEnd(showInstallStatus ? 22 : 24)} │ ${pkg.description.length > 47
8
- ? `${pkg.description.slice(0, 44)}...`
9
- : pkg.description.padEnd(49)} │ ${pkg.vendor.padEnd(19)} │ ${pkg.license.padEnd(14)}`,
28
+ name: `${prefix}${truncatedName} ${truncatedId} ${truncatedDesc} │ ${truncatedVendor}`,
10
29
  value: pkg,
11
- short: pkg.name,
30
+ short: name,
12
31
  };
13
32
  }
14
33
  export function createPackagePrompt(packages, options = {}) {
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@smithery/cli",
3
- "version": "0.0.3",
3
+ "version": "0.0.9",
4
+ "private": false,
4
5
  "homepage": "https://smithery.ai/",
5
6
  "description": "A NPX command to install and list Model Context Protocols",
6
7
  "main": "dist/index.js",
@@ -24,7 +25,7 @@
24
25
  "format": "npx @biomejs/biome format --write"
25
26
  },
26
27
  "bin": {
27
- "@smithery/cli": "dist/index.js"
28
+ "cli": "dist/index.js"
28
29
  },
29
30
  "dependencies": {
30
31
  "@iarna/toml": "^2.2.5",
@@ -1,209 +0,0 @@
1
- import { exec } from 'child_process';
2
- import { promisify } from 'util';
3
- import * as fs from 'fs';
4
- import * as path from 'path';
5
- import { fileURLToPath } from 'url';
6
- import { dirname } from 'path';
7
- import * as TOML from '@iarna/toml';
8
- const execAsync = promisify(exec);
9
- const __filename = fileURLToPath(import.meta.url);
10
- const __dirname = dirname(__filename);
11
- const REPOS = [
12
- {
13
- url: 'https://github.com/modelcontextprotocol/servers.git',
14
- branch: 'main',
15
- packagePath: 'src',
16
- runtime: 'mixed'
17
- },
18
- {
19
- url: 'https://github.com/mcp-get/community-servers.git',
20
- branch: 'main',
21
- packagePath: 'src',
22
- runtime: 'mixed'
23
- }
24
- ];
25
- async function cloneRepo(config, tempDir) {
26
- const repoDir = path.join(tempDir, path.basename(config.url, '.git'));
27
- console.log(`Cloning ${config.url} into ${repoDir}...`);
28
- await execAsync(`git clone --depth 1 --branch ${config.branch} ${config.url} ${repoDir}`, { cwd: tempDir });
29
- return;
30
- }
31
- async function extractNodePackage(packageJsonPath, repoUrl, subPath) {
32
- try {
33
- const packageData = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
34
- const repoPath = `${repoUrl}/blob/main/${subPath}`;
35
- return {
36
- name: packageData.name || '',
37
- description: packageData.description || '',
38
- vendor: packageData.author || '',
39
- sourceUrl: repoPath,
40
- homepage: packageData.homepage || '',
41
- license: packageData.license || '',
42
- runtime: 'node'
43
- };
44
- }
45
- catch (error) {
46
- console.error(`Error extracting Node package from ${packageJsonPath}:`, error);
47
- return null;
48
- }
49
- }
50
- async function extractPythonPackage(pyprojectPath, repoUrl, subPath) {
51
- try {
52
- const pyprojectContent = fs.readFileSync(pyprojectPath, 'utf8');
53
- const pyproject = TOML.parse(pyprojectContent);
54
- if (!pyproject.project) {
55
- console.error(`No [project] section found in ${pyprojectPath}`);
56
- return null;
57
- }
58
- const { project } = pyproject;
59
- // Set vendor to Anthropic
60
- const vendor = 'Anthropic, PBC (https://anthropic.com)';
61
- // Set license to MIT
62
- const license = 'MIT';
63
- return {
64
- name: project.name || '',
65
- description: project.description || '',
66
- vendor,
67
- sourceUrl: `${repoUrl}/blob/main/${subPath}`,
68
- homepage: project.homepage || project.repository || repoUrl,
69
- license,
70
- runtime: 'python'
71
- };
72
- }
73
- catch (error) {
74
- console.error(`Error extracting Python package from ${pyprojectPath}:`, error);
75
- return null;
76
- }
77
- }
78
- export async function extractPackageInfo() {
79
- const tempDir = path.join(__dirname, '../../temp');
80
- const outputPath = path.join(__dirname, '../../packages/package-list.json');
81
- const commitMsgPath = path.join(__dirname, '../../temp/commit-msg.txt');
82
- console.log("Starting package extraction...");
83
- try {
84
- // Load existing packages
85
- let existingPackages = [];
86
- if (fs.existsSync(outputPath)) {
87
- existingPackages = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
88
- }
89
- // Create temp directory if it doesn't exist
90
- if (!fs.existsSync(tempDir)) {
91
- fs.mkdirSync(tempDir, { recursive: true });
92
- }
93
- // Initialize commit message
94
- let commitMsg = "chore(packages): update MCP package list\n\nChanges:\n";
95
- let changes = [];
96
- // Process each repository
97
- const newPackages = [];
98
- for (const repo of REPOS) {
99
- await cloneRepo(repo, tempDir);
100
- const repoDir = path.join(tempDir, path.basename(repo.url, '.git'));
101
- const packagePath = path.join(repoDir, repo.packagePath);
102
- if (!fs.existsSync(packagePath))
103
- continue;
104
- const dirs = fs.readdirSync(packagePath);
105
- for (const dir of dirs) {
106
- const fullPath = path.join(packagePath, dir);
107
- if (!fs.statSync(fullPath).isDirectory())
108
- continue;
109
- // Try to extract as Node.js package
110
- const packageJsonPath = path.join(fullPath, 'package.json');
111
- if (fs.existsSync(packageJsonPath)) {
112
- const nodePackage = await extractNodePackage(packageJsonPath, repo.url.replace('.git', ''), path.join(repo.packagePath, dir));
113
- if (nodePackage) {
114
- newPackages.push(nodePackage);
115
- continue;
116
- }
117
- }
118
- // Try to extract as Python package
119
- const pyprojectPath = path.join(fullPath, 'pyproject.toml');
120
- if (fs.existsSync(pyprojectPath)) {
121
- const pythonPackage = await extractPythonPackage(pyprojectPath, repo.url.replace('.git', ''), path.join(repo.packagePath, dir));
122
- if (pythonPackage) {
123
- newPackages.push(pythonPackage);
124
- }
125
- }
126
- }
127
- }
128
- // Merge existing and new packages
129
- const mergedPackages = [...existingPackages];
130
- let hasChanges = false;
131
- for (const newPkg of newPackages) {
132
- const existingIndex = mergedPackages.findIndex(pkg => pkg.name === newPkg.name);
133
- if (existingIndex >= 0) {
134
- // Update existing package if there are changes
135
- if (JSON.stringify(mergedPackages[existingIndex]) !== JSON.stringify(newPkg)) {
136
- const oldPkg = mergedPackages[existingIndex];
137
- mergedPackages[existingIndex] = newPkg;
138
- hasChanges = true;
139
- console.log(`Updated package: ${newPkg.name}`);
140
- // Add detailed change information
141
- const changeDetails = [];
142
- if (oldPkg.description !== newPkg.description)
143
- changeDetails.push('description');
144
- if (oldPkg.vendor !== newPkg.vendor)
145
- changeDetails.push('vendor');
146
- if (oldPkg.license !== newPkg.license)
147
- changeDetails.push('license');
148
- if (oldPkg.homepage !== newPkg.homepage)
149
- changeDetails.push('homepage');
150
- if (oldPkg.sourceUrl !== newPkg.sourceUrl)
151
- changeDetails.push('sourceUrl');
152
- if (oldPkg.runtime !== newPkg.runtime)
153
- changeDetails.push('runtime');
154
- changes.push(`- Updated ${newPkg.name} (changed: ${changeDetails.join(', ')})`);
155
- }
156
- }
157
- else {
158
- // Add new package
159
- mergedPackages.push(newPkg);
160
- hasChanges = true;
161
- console.log(`Added new package: ${newPkg.name} (${newPkg.runtime})`);
162
- changes.push(`- Added new package: ${newPkg.name} (${newPkg.runtime})`);
163
- }
164
- }
165
- // Write updated packages to file if there are changes
166
- if (hasChanges) {
167
- fs.writeFileSync(outputPath, JSON.stringify(mergedPackages, null, 2));
168
- console.log('Package list updated successfully');
169
- // Write commit message with total number of changes
170
- if (changes.length === 0) {
171
- changes.push('- Initial package list creation');
172
- }
173
- commitMsg = `chore(packages): update MCP package list (${changes.length} packages)\n\nChanges:\n`;
174
- commitMsg += changes.join('\n');
175
- fs.writeFileSync(commitMsgPath, commitMsg);
176
- console.log('Commit message generated');
177
- }
178
- else {
179
- console.log('No changes detected in package list');
180
- }
181
- // Cleanup (but keep commit-msg.txt if it exists)
182
- const filesToKeep = new Set(['commit-msg.txt']);
183
- for (const file of fs.readdirSync(tempDir)) {
184
- if (!filesToKeep.has(file)) {
185
- const filePath = path.join(tempDir, file);
186
- fs.rmSync(filePath, { recursive: true, force: true });
187
- }
188
- }
189
- console.log('Temporary files cleaned up');
190
- return mergedPackages;
191
- }
192
- catch (error) {
193
- console.error('Error:', error);
194
- // Cleanup on error
195
- if (fs.existsSync(tempDir)) {
196
- fs.rmSync(tempDir, { recursive: true, force: true });
197
- }
198
- throw error;
199
- }
200
- }
201
- // Run the function if this file is executed directly
202
- if (import.meta.url === `file://${__filename}`) {
203
- extractPackageInfo()
204
- .then(() => console.log('Package extraction completed'))
205
- .catch(error => {
206
- console.error('Failed to extract packages:', error);
207
- process.exit(1);
208
- });
209
- }
@@ -1,94 +0,0 @@
1
- export const packageHelpers = {
2
- "@modelcontextprotocol/server-brave-search": {
3
- requiredEnvVars: {
4
- BRAVE_API_KEY: {
5
- description: "API key for Brave Search",
6
- required: true,
7
- },
8
- },
9
- },
10
- "@modelcontextprotocol/server-github": {
11
- requiredEnvVars: {
12
- GITHUB_PERSONAL_ACCESS_TOKEN: {
13
- description: "Personal access token for GitHub API access",
14
- required: true,
15
- },
16
- },
17
- },
18
- "@modelcontextprotocol/server-gitlab": {
19
- requiredEnvVars: {
20
- GITLAB_PERSONAL_ACCESS_TOKEN: {
21
- description: "Personal access token for GitLab API access",
22
- required: true,
23
- },
24
- GITLAB_API_URL: {
25
- description: "GitLab API URL (optional, for self-hosted instances)",
26
- required: false,
27
- },
28
- },
29
- },
30
- "@modelcontextprotocol/server-google-maps": {
31
- requiredEnvVars: {
32
- GOOGLE_MAPS_API_KEY: {
33
- description: "API key for Google Maps services",
34
- required: true,
35
- },
36
- },
37
- },
38
- "@modelcontextprotocol/server-slack": {
39
- requiredEnvVars: {
40
- SLACK_BOT_TOKEN: {
41
- description: "Slack Bot User OAuth Token (starts with xoxb-)",
42
- required: true,
43
- },
44
- SLACK_TEAM_ID: {
45
- description: "Slack Team/Workspace ID",
46
- required: true,
47
- },
48
- },
49
- },
50
- "@raygun.io/mcp-server-raygun": {
51
- requiredEnvVars: {
52
- RAYGUN_PAT_TOKEN: {
53
- description: "Personal access token for Raygun API access",
54
- required: true,
55
- },
56
- },
57
- },
58
- "@kagi/mcp-server-kagi": {
59
- requiredEnvVars: {
60
- KAGI_API_KEY: {
61
- description: "API key for Kagi Search",
62
- required: true,
63
- },
64
- },
65
- },
66
- "@exa/mcp-server": {
67
- requiredEnvVars: {
68
- EXA_API_KEY: {
69
- description: "API key for Exa AI Search",
70
- required: true,
71
- },
72
- },
73
- },
74
- "@search1api/mcp-server": {
75
- requiredEnvVars: {
76
- SEARCH1API_KEY: {
77
- description: "API key for Search1API",
78
- required: true,
79
- },
80
- },
81
- },
82
- "mcp-tinybird": {
83
- requiredEnvVars: {
84
- TB_API_URL: {
85
- description: "API URL for Tinybird",
86
- required: true,
87
- },
88
- TB_ADMIN_TOKEN: {
89
- description: "Admin token for Tinybird",
90
- required: true,
91
- },
92
- },
93
- },
94
- };