mcpick 0.0.15 → 0.0.16

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/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # mcpick
2
2
 
3
+ ## 0.0.16
4
+
5
+ ### Patch Changes
6
+
7
+ - b671135: fix: validate marketplace repos upfront, remove unsupported
8
+ --scope flag
9
+ - fed8311: feat: TUI marketplace management, plugin browse, CLI parity
10
+ commands
11
+
3
12
  ## 0.0.15
4
13
 
5
14
  ### Patch Changes
@@ -0,0 +1,60 @@
1
+ import { defineCommand } from 'citty';
2
+ import { mcp_add_json_via_cli } from '../../utils/claude-cli.js';
3
+ import { error, output } from '../output.js';
4
+ export default defineCommand({
5
+ meta: {
6
+ name: 'add-json',
7
+ description: 'Add an MCP server from a JSON configuration string',
8
+ },
9
+ args: {
10
+ name: {
11
+ type: 'positional',
12
+ description: 'Server name',
13
+ required: true,
14
+ },
15
+ config: {
16
+ type: 'positional',
17
+ description: 'JSON configuration string',
18
+ required: true,
19
+ },
20
+ scope: {
21
+ type: 'string',
22
+ description: 'Scope: local, project, or user (default: local)',
23
+ default: 'local',
24
+ },
25
+ json: {
26
+ type: 'boolean',
27
+ description: 'Output as JSON',
28
+ default: false,
29
+ },
30
+ },
31
+ async run({ args }) {
32
+ const scope = args.scope;
33
+ if (!['local', 'project', 'user'].includes(scope)) {
34
+ error(`Invalid scope: ${scope}. Use local, project, or user.`);
35
+ }
36
+ // Validate the JSON is parseable
37
+ try {
38
+ JSON.parse(args.config);
39
+ }
40
+ catch {
41
+ error('Invalid JSON configuration. Provide a valid JSON string.');
42
+ }
43
+ const result = await mcp_add_json_via_cli(args.name, args.config, scope);
44
+ if (args.json) {
45
+ output({
46
+ added: args.name,
47
+ scope,
48
+ success: result.success,
49
+ error: result.error,
50
+ }, true);
51
+ }
52
+ else if (result.success) {
53
+ console.log(`Added '${args.name}' from JSON (scope: ${scope})`);
54
+ }
55
+ else {
56
+ error(result.error || 'Unknown error');
57
+ }
58
+ },
59
+ });
60
+ //# sourceMappingURL=add-json.js.map
@@ -0,0 +1,45 @@
1
+ import { defineCommand } from 'citty';
2
+ import { mcp_get_via_cli } from '../../utils/claude-cli.js';
3
+ import { error, output } from '../output.js';
4
+ export default defineCommand({
5
+ meta: {
6
+ name: 'get',
7
+ description: 'Get details about an MCP server',
8
+ },
9
+ args: {
10
+ name: {
11
+ type: 'positional',
12
+ description: 'Server name',
13
+ required: true,
14
+ },
15
+ json: {
16
+ type: 'boolean',
17
+ description: 'Output as JSON',
18
+ default: false,
19
+ },
20
+ },
21
+ async run({ args }) {
22
+ const result = await mcp_get_via_cli(args.name);
23
+ if (args.json) {
24
+ try {
25
+ const parsed = JSON.parse(result.stdout || '{}');
26
+ output(parsed, true);
27
+ }
28
+ catch {
29
+ output({
30
+ name: args.name,
31
+ success: result.success,
32
+ output: result.stdout,
33
+ error: result.error,
34
+ }, true);
35
+ }
36
+ }
37
+ else if (result.success) {
38
+ console.log(result.stdout || 'No details available.');
39
+ }
40
+ else {
41
+ error(result.error || 'Unknown error');
42
+ }
43
+ },
44
+ });
45
+ //# sourceMappingURL=get.js.map
@@ -1,4 +1,5 @@
1
1
  import { defineCommand } from 'citty';
2
+ import { read_marketplace_manifest } from '../../core/plugin-cache.js';
2
3
  import { marketplace_add_via_cli, marketplace_list_via_cli, marketplace_remove_via_cli, marketplace_update_via_cli, } from '../../utils/claude-cli.js';
3
4
  import { error, output } from '../output.js';
4
5
  const list = defineCommand({
@@ -36,7 +37,7 @@ const list = defineCommand({
36
37
  const add = defineCommand({
37
38
  meta: {
38
39
  name: 'add',
39
- description: 'Add a marketplace from URL, path, or GitHub repo',
40
+ description: 'Add a plugin marketplace (a catalog of installable plugins)',
40
41
  },
41
42
  args: {
42
43
  source: {
@@ -44,11 +45,6 @@ const add = defineCommand({
44
45
  description: 'Marketplace source (GitHub repo, URL, or local path)',
45
46
  required: true,
46
47
  },
47
- scope: {
48
- type: 'string',
49
- description: 'Scope: user, project, or local (default: user)',
50
- default: 'user',
51
- },
52
48
  json: {
53
49
  type: 'boolean',
54
50
  description: 'Output as JSON',
@@ -56,27 +52,72 @@ const add = defineCommand({
56
52
  },
57
53
  },
58
54
  async run({ args }) {
59
- const scope = args.scope;
60
- if (!['user', 'project', 'local'].includes(scope)) {
61
- error(`Invalid scope: ${scope}. Use user, project, or local.`);
62
- }
63
- const result = await marketplace_add_via_cli(args.source, scope);
55
+ const result = await marketplace_add_via_cli(args.source);
64
56
  if (args.json) {
57
+ // Try to include available plugins in JSON output
58
+ let available_plugins = [];
59
+ if (result.success) {
60
+ const manifests = await find_marketplace_plugins(args.source);
61
+ available_plugins = manifests;
62
+ }
65
63
  output({
66
64
  added: args.source,
67
- scope,
68
65
  success: result.success,
69
66
  error: result.error,
67
+ available_plugins,
70
68
  }, true);
71
69
  }
72
70
  else if (result.success) {
73
- console.log(`Marketplace added: ${args.source} (scope: ${scope})`);
71
+ console.log(`Marketplace added: ${args.source}`);
72
+ await show_available_plugins(args.source);
74
73
  }
75
74
  else {
76
75
  error(result.error || 'Unknown error');
77
76
  }
78
77
  },
79
78
  });
79
+ /**
80
+ * Try to find and display available plugins from a newly added marketplace.
81
+ * The marketplace name in the filesystem may differ from the source string,
82
+ * so we try common derivations.
83
+ */
84
+ async function find_marketplace_plugins(source) {
85
+ // Try the source as-is, then extract repo name from various formats
86
+ const candidates = [];
87
+ // Extract repo name from owner/repo, URLs, etc.
88
+ const repo_match = source.match(/([^/]+?)(?:\.git)?$/);
89
+ if (repo_match) {
90
+ candidates.push(repo_match[1].toLowerCase());
91
+ candidates.push(repo_match[1]);
92
+ }
93
+ // Try the full source as a name
94
+ candidates.push(source);
95
+ for (const name of candidates) {
96
+ const manifest = await read_marketplace_manifest(name);
97
+ if (manifest?.plugins?.length) {
98
+ return manifest.plugins.map((p) => {
99
+ const desc = p.description
100
+ ? ` - ${p.description}`
101
+ : '';
102
+ return `${p.name}${desc}`;
103
+ });
104
+ }
105
+ }
106
+ return [];
107
+ }
108
+ async function show_available_plugins(source) {
109
+ const plugins = await find_marketplace_plugins(source);
110
+ if (plugins.length > 0) {
111
+ console.log(`\nAvailable plugins (${plugins.length}):`);
112
+ for (const p of plugins) {
113
+ console.log(` - ${p}`);
114
+ }
115
+ console.log('\nInstall with: mcpick plugins install <name>@<marketplace>');
116
+ }
117
+ else {
118
+ console.log('\nTo browse and install plugins: mcpick plugins install <name>@<marketplace>');
119
+ }
120
+ }
80
121
  const remove = defineCommand({
81
122
  meta: {
82
123
  name: 'remove',
@@ -150,7 +191,7 @@ const update = defineCommand({
150
191
  export default defineCommand({
151
192
  meta: {
152
193
  name: 'marketplace',
153
- description: 'Manage Claude Code plugin marketplaces',
194
+ description: 'Manage plugin marketplaces (catalogs of installable plugins). Add a marketplace first, then install plugins from it with: mcpick plugins install <name>@<marketplace>',
154
195
  },
155
196
  subCommands: {
156
197
  list,
@@ -1,7 +1,7 @@
1
1
  import { defineCommand } from 'citty';
2
2
  import { parse_plugin_key, read_known_marketplaces, } from '../../core/plugin-cache.js';
3
3
  import { build_enabled_plugins, get_all_plugins, read_claude_settings, write_claude_settings, } from '../../core/settings.js';
4
- import { install_plugin_via_cli, uninstall_plugin_via_cli, update_plugin_via_cli, } from '../../utils/claude-cli.js';
4
+ import { install_plugin_via_cli, uninstall_plugin_via_cli, update_plugin_via_cli, validate_plugin_via_cli, } from '../../utils/claude-cli.js';
5
5
  import { error, output } from '../output.js';
6
6
  const list = defineCommand({
7
7
  meta: {
@@ -212,11 +212,54 @@ const update = defineCommand({
212
212
  }
213
213
  },
214
214
  });
215
+ const validate = defineCommand({
216
+ meta: {
217
+ name: 'validate',
218
+ description: 'Validate a plugin or marketplace manifest',
219
+ },
220
+ args: {
221
+ path: {
222
+ type: 'positional',
223
+ description: 'Path to plugin or marketplace manifest',
224
+ required: true,
225
+ },
226
+ json: {
227
+ type: 'boolean',
228
+ description: 'Output as JSON',
229
+ default: false,
230
+ },
231
+ },
232
+ async run({ args }) {
233
+ const result = await validate_plugin_via_cli(args.path);
234
+ if (args.json) {
235
+ output({
236
+ path: args.path,
237
+ valid: result.success,
238
+ output: result.stdout,
239
+ error: result.error,
240
+ }, true);
241
+ }
242
+ else if (result.success) {
243
+ console.log(result.stdout || 'Validation passed.');
244
+ }
245
+ else {
246
+ error(result.error || 'Validation failed');
247
+ }
248
+ },
249
+ });
215
250
  export default defineCommand({
216
251
  meta: {
217
252
  name: 'plugins',
218
253
  description: 'Manage Claude Code plugins',
219
254
  },
220
- subCommands: { list, enable, disable, install, uninstall, update },
255
+ subCommands: {
256
+ list,
257
+ enable,
258
+ disable,
259
+ install,
260
+ uninstall,
261
+ update,
262
+ validate,
263
+ },
221
264
  });
222
265
  //# sourceMappingURL=plugins.js.map
@@ -0,0 +1,32 @@
1
+ import { defineCommand } from 'citty';
2
+ import { mcp_reset_project_choices_via_cli } from '../../utils/claude-cli.js';
3
+ import { error, output } from '../output.js';
4
+ export default defineCommand({
5
+ meta: {
6
+ name: 'reset-project-choices',
7
+ description: 'Reset all approved and rejected project-scoped MCP servers',
8
+ },
9
+ args: {
10
+ json: {
11
+ type: 'boolean',
12
+ description: 'Output as JSON',
13
+ default: false,
14
+ },
15
+ },
16
+ async run({ args }) {
17
+ const result = await mcp_reset_project_choices_via_cli();
18
+ if (args.json) {
19
+ output({
20
+ success: result.success,
21
+ error: result.error,
22
+ }, true);
23
+ }
24
+ else if (result.success) {
25
+ console.log('Project MCP server choices have been reset.');
26
+ }
27
+ else {
28
+ error(result.error || 'Unknown error');
29
+ }
30
+ },
31
+ });
32
+ //# sourceMappingURL=reset-project-choices.js.map
package/dist/cli/index.js CHANGED
@@ -10,6 +10,9 @@ const main = defineCommand({
10
10
  disable: () => import('./commands/disable.js').then((m) => m.default),
11
11
  remove: () => import('./commands/remove.js').then((m) => m.default),
12
12
  add: () => import('./commands/add.js').then((m) => m.default),
13
+ 'add-json': () => import('./commands/add-json.js').then((m) => m.default),
14
+ get: () => import('./commands/get.js').then((m) => m.default),
15
+ 'reset-project-choices': () => import('./commands/reset-project-choices.js').then((m) => m.default),
13
16
  backup: () => import('./commands/backup.js').then((m) => m.default),
14
17
  restore: () => import('./commands/restore.js').then((m) => m.default),
15
18
  profile: () => import('./commands/profile.js').then((m) => m.default),
@@ -0,0 +1,275 @@
1
+ import { confirm, isCancel, log, multiselect, note, select, text, } from '@clack/prompts';
2
+ import { read_known_marketplaces, read_marketplace_manifest, } from '../core/plugin-cache.js';
3
+ import { get_all_plugins, read_claude_settings, } from '../core/settings.js';
4
+ import { install_plugin_via_cli, marketplace_add_via_cli, marketplace_remove_via_cli, marketplace_update_via_cli, uninstall_plugin_via_cli, } from '../utils/claude-cli.js';
5
+ /**
6
+ * Browse all available plugins across all marketplaces.
7
+ * Shows a multiselect with installed plugins pre-selected — toggle to install/uninstall.
8
+ */
9
+ async function handle_browse() {
10
+ const known = await read_known_marketplaces();
11
+ const marketplace_names = Object.keys(known);
12
+ if (marketplace_names.length === 0) {
13
+ note('No marketplaces configured.\nAdd one first to browse plugins.');
14
+ return;
15
+ }
16
+ // Build list of all available plugins across marketplaces
17
+ const all_available = [];
18
+ for (const mkt_name of marketplace_names) {
19
+ const manifest = await read_marketplace_manifest(mkt_name);
20
+ if (!manifest?.plugins?.length)
21
+ continue;
22
+ for (const p of manifest.plugins) {
23
+ all_available.push({
24
+ key: `${p.name}@${mkt_name}`,
25
+ name: p.name,
26
+ marketplace: mkt_name,
27
+ description: p.description,
28
+ });
29
+ }
30
+ }
31
+ if (all_available.length === 0) {
32
+ note('No plugins found in any marketplace.');
33
+ return;
34
+ }
35
+ // Get currently installed plugins
36
+ const settings = await read_claude_settings();
37
+ const installed = get_all_plugins(settings);
38
+ const installed_keys = new Set(installed.map((p) => `${p.name}@${p.marketplace}`));
39
+ const selected = await multiselect({
40
+ message: `Available plugins (${all_available.length}) — toggle to install/uninstall:`,
41
+ options: all_available.map((p) => ({
42
+ value: p.key,
43
+ label: p.name,
44
+ hint: `${p.marketplace}${p.description ? ` · ${p.description}` : ''}`,
45
+ })),
46
+ initialValues: all_available
47
+ .filter((p) => installed_keys.has(p.key))
48
+ .map((p) => p.key),
49
+ required: false,
50
+ });
51
+ if (isCancel(selected))
52
+ return;
53
+ const selected_set = new Set(selected);
54
+ // Determine what to install and uninstall
55
+ const to_install = all_available.filter((p) => selected_set.has(p.key) && !installed_keys.has(p.key));
56
+ const to_uninstall = all_available.filter((p) => !selected_set.has(p.key) && installed_keys.has(p.key));
57
+ if (to_install.length === 0 && to_uninstall.length === 0) {
58
+ log.info('No changes.');
59
+ return;
60
+ }
61
+ // Install new plugins
62
+ for (const p of to_install) {
63
+ log.info(`Installing ${p.key}...`);
64
+ const result = await install_plugin_via_cli(p.key);
65
+ if (result.success) {
66
+ log.success(`Installed: ${p.key}`);
67
+ }
68
+ else {
69
+ log.error(`Failed: ${p.key} - ${result.error}`);
70
+ }
71
+ }
72
+ // Uninstall deselected plugins
73
+ for (const p of to_uninstall) {
74
+ log.info(`Uninstalling ${p.key}...`);
75
+ const result = await uninstall_plugin_via_cli(p.key);
76
+ if (result.success) {
77
+ log.success(`Uninstalled: ${p.key}`);
78
+ }
79
+ else {
80
+ log.error(`Failed: ${p.key} - ${result.error}`);
81
+ }
82
+ }
83
+ const parts = [];
84
+ if (to_install.length > 0)
85
+ parts.push(`${to_install.length} installed`);
86
+ if (to_uninstall.length > 0)
87
+ parts.push(`${to_uninstall.length} uninstalled`);
88
+ note(parts.join(', '), 'Plugins updated');
89
+ }
90
+ async function handle_add() {
91
+ const source = await text({
92
+ message: 'Marketplace source:',
93
+ placeholder: 'e.g. owner/repo or https://github.com/owner/repo',
94
+ validate: (value) => {
95
+ if (!value || value.trim().length === 0) {
96
+ return 'Marketplace source is required';
97
+ }
98
+ },
99
+ });
100
+ if (isCancel(source))
101
+ return;
102
+ log.info(`Adding marketplace: ${source}`);
103
+ const result = await marketplace_add_via_cli(source);
104
+ if (!result.success) {
105
+ log.error(result.error || 'Unknown error');
106
+ return;
107
+ }
108
+ log.success(`Marketplace added: ${source}`);
109
+ // Try to find and offer available plugins
110
+ const marketplace_name = derive_marketplace_name(source);
111
+ const manifest = marketplace_name
112
+ ? await read_marketplace_manifest(marketplace_name)
113
+ : null;
114
+ if (!manifest?.plugins?.length) {
115
+ log.info('Install plugins with: mcpick plugins install <name>@<marketplace>');
116
+ return;
117
+ }
118
+ const should_install = await confirm({
119
+ message: `${manifest.plugins.length} plugins available. Install now?`,
120
+ });
121
+ if (isCancel(should_install) || !should_install)
122
+ return;
123
+ const to_install = await multiselect({
124
+ message: 'Select plugins to install:',
125
+ options: manifest.plugins.map((p) => ({
126
+ value: `${p.name}@${marketplace_name}`,
127
+ label: p.name,
128
+ hint: p.description,
129
+ })),
130
+ required: false,
131
+ });
132
+ if (isCancel(to_install) || to_install.length === 0)
133
+ return;
134
+ for (const key of to_install) {
135
+ log.info(`Installing ${key}...`);
136
+ const install_result = await install_plugin_via_cli(key);
137
+ if (install_result.success) {
138
+ log.success(`Installed: ${key}`);
139
+ }
140
+ else {
141
+ log.error(`Failed: ${key} - ${install_result.error}`);
142
+ }
143
+ }
144
+ }
145
+ function derive_marketplace_name(source) {
146
+ const match = source.match(/([^/]+?)(?:\.git)?$/);
147
+ return match ? match[1].toLowerCase() : null;
148
+ }
149
+ async function handle_remove() {
150
+ const known = await read_known_marketplaces();
151
+ const names = Object.keys(known);
152
+ if (names.length === 0) {
153
+ note('No marketplaces configured.');
154
+ return;
155
+ }
156
+ const name = await select({
157
+ message: 'Select marketplace to remove:',
158
+ options: names.map((n) => ({
159
+ value: n,
160
+ label: n,
161
+ hint: known[n].source.repo || known[n].source.url,
162
+ })),
163
+ });
164
+ if (isCancel(name))
165
+ return;
166
+ const should_remove = await confirm({
167
+ message: `Remove marketplace '${name}'? Its plugins will also be removed.`,
168
+ });
169
+ if (isCancel(should_remove) || !should_remove)
170
+ return;
171
+ const remove_result = await marketplace_remove_via_cli(name);
172
+ if (remove_result.success) {
173
+ log.success(`Marketplace removed: ${name}`);
174
+ }
175
+ else {
176
+ log.error(remove_result.error || 'Unknown error');
177
+ }
178
+ }
179
+ async function handle_update() {
180
+ const known = await read_known_marketplaces();
181
+ const names = Object.keys(known);
182
+ if (names.length === 0) {
183
+ note('No marketplaces configured.');
184
+ return;
185
+ }
186
+ const choice = await select({
187
+ message: 'What to update:',
188
+ options: [
189
+ { value: '__all__', label: 'All marketplaces' },
190
+ ...names.map((n) => ({
191
+ value: n,
192
+ label: n,
193
+ hint: known[n].source.repo || known[n].source.url,
194
+ })),
195
+ ],
196
+ });
197
+ if (isCancel(choice))
198
+ return;
199
+ if (choice === '__all__') {
200
+ log.info('Updating all marketplaces...');
201
+ const result = await marketplace_update_via_cli();
202
+ if (result.success) {
203
+ log.success('All marketplaces updated.');
204
+ }
205
+ else {
206
+ log.error(result.error || 'Unknown error');
207
+ }
208
+ }
209
+ else {
210
+ log.info(`Updating ${choice}...`);
211
+ const result = await marketplace_update_via_cli(choice);
212
+ if (result.success) {
213
+ log.success(`Marketplace updated: ${choice}`);
214
+ }
215
+ else {
216
+ log.error(result.error || 'Unknown error');
217
+ }
218
+ }
219
+ }
220
+ export async function manage_marketplace() {
221
+ while (true) {
222
+ const action = await select({
223
+ message: 'Marketplace & plugins:',
224
+ options: [
225
+ {
226
+ value: 'browse',
227
+ label: 'Browse & install plugins',
228
+ hint: 'Toggle plugins on/off across all marketplaces',
229
+ },
230
+ {
231
+ value: 'add',
232
+ label: 'Add marketplace',
233
+ hint: 'Add a plugin catalog, then install plugins from it',
234
+ },
235
+ {
236
+ value: 'remove',
237
+ label: 'Remove marketplace',
238
+ hint: 'Remove a marketplace and its plugins',
239
+ },
240
+ {
241
+ value: 'update',
242
+ label: 'Update marketplace(s)',
243
+ hint: 'Pull latest from source',
244
+ },
245
+ {
246
+ value: 'back',
247
+ label: 'Back',
248
+ hint: 'Return to main menu',
249
+ },
250
+ ],
251
+ });
252
+ if (isCancel(action) || action === 'back')
253
+ return;
254
+ try {
255
+ switch (action) {
256
+ case 'browse':
257
+ await handle_browse();
258
+ break;
259
+ case 'add':
260
+ await handle_add();
261
+ break;
262
+ case 'remove':
263
+ await handle_remove();
264
+ break;
265
+ case 'update':
266
+ await handle_update();
267
+ break;
268
+ }
269
+ }
270
+ catch (err) {
271
+ log.error(err instanceof Error ? err.message : 'Unknown error');
272
+ }
273
+ }
274
+ }
275
+ //# sourceMappingURL=manage-marketplace.js.map
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import { backup_config } from './commands/backup.js';
5
5
  import { edit_config } from './commands/edit-config.js';
6
6
  import { edit_plugins } from './commands/edit-plugins.js';
7
7
  import { manage_cache } from './commands/manage-cache.js';
8
+ import { manage_marketplace } from './commands/manage-marketplace.js';
8
9
  import { restore_config } from './commands/restore.js';
9
10
  import { write_claude_config } from './core/config.js';
10
11
  import { list_profiles, load_profile, save_profile, } from './core/profile.js';
@@ -178,6 +179,11 @@ async function main() {
178
179
  label: 'Manage plugins',
179
180
  hint: 'Toggle, install, uninstall, or update plugins',
180
181
  },
182
+ {
183
+ value: 'manage-marketplace',
184
+ label: 'Manage marketplaces',
185
+ hint: 'Add, remove, or update plugin marketplaces',
186
+ },
181
187
  {
182
188
  value: 'manage-cache',
183
189
  label: 'Manage plugin cache',
@@ -226,6 +232,9 @@ async function main() {
226
232
  case 'edit-plugins':
227
233
  await edit_plugins();
228
234
  break;
235
+ case 'manage-marketplace':
236
+ await manage_marketplace();
237
+ break;
229
238
  case 'manage-cache':
230
239
  await manage_cache();
231
240
  break;
@@ -276,6 +285,9 @@ const SUBCOMMANDS = new Set([
276
285
  'disable',
277
286
  'remove',
278
287
  'add',
288
+ 'add-json',
289
+ 'get',
290
+ 'reset-project-choices',
279
291
  'backup',
280
292
  'restore',
281
293
  'profile',
@@ -192,7 +192,51 @@ export async function update_plugin_via_cli(key, scope = 'user') {
192
192
  /**
193
193
  * Add a marketplace via Claude CLI
194
194
  */
195
- export async function marketplace_add_via_cli(source, scope = 'user') {
195
+ /**
196
+ * Extract GitHub owner/repo from various source formats.
197
+ * Returns null if not a recognizable GitHub reference.
198
+ */
199
+ function parse_github_repo(source) {
200
+ // HTTPS URL: https://github.com/owner/repo[.git]
201
+ const https_match = source.match(/^https?:\/\/github\.com\/([^/]+)\/([^/.]+)(?:\.git)?$/);
202
+ if (https_match)
203
+ return { owner: https_match[1], repo: https_match[2] };
204
+ // SSH URL: git@github.com:owner/repo[.git]
205
+ const ssh_match = source.match(/^git@github\.com:([^/]+)\/([^/.]+)(?:\.git)?$/);
206
+ if (ssh_match)
207
+ return { owner: ssh_match[1], repo: ssh_match[2] };
208
+ // Shorthand: owner/repo (no slashes beyond the one separator)
209
+ const shorthand_match = source.match(/^([^/\s]+)\/([^/\s]+)$/);
210
+ if (shorthand_match)
211
+ return { owner: shorthand_match[1], repo: shorthand_match[2] };
212
+ return null;
213
+ }
214
+ /**
215
+ * Validate that a GitHub repository exists and is accessible.
216
+ * Returns an error message if validation fails, null if OK.
217
+ */
218
+ async function validate_github_repo(owner, repo) {
219
+ try {
220
+ const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
221
+ method: 'GET',
222
+ headers: { Accept: 'application/vnd.github.v3+json' },
223
+ });
224
+ if (response.status === 200)
225
+ return null;
226
+ if (response.status === 404) {
227
+ return `Repository '${owner}/${repo}' not found on GitHub. Check the name or ensure it's not private.`;
228
+ }
229
+ if (response.status === 403) {
230
+ return `Access denied for '${owner}/${repo}'. The repository may be private — configure a GitHub token or use SSH.`;
231
+ }
232
+ return `GitHub API returned status ${response.status} for '${owner}/${repo}'.`;
233
+ }
234
+ catch {
235
+ // Network error — skip validation and let the CLI attempt the clone
236
+ return null;
237
+ }
238
+ }
239
+ export async function marketplace_add_via_cli(source) {
196
240
  const cli_available = await check_claude_cli();
197
241
  if (!cli_available) {
198
242
  return {
@@ -200,12 +244,37 @@ export async function marketplace_add_via_cli(source, scope = 'user') {
200
244
  error: 'Claude CLI not found. Please install Claude Code CLI.',
201
245
  };
202
246
  }
247
+ // Validate GitHub repo exists before attempting clone
248
+ // Only validate shorthand (owner/repo) — explicit URLs imply the user knows the repo
249
+ const gh = parse_github_repo(source);
250
+ const is_shorthand = gh && !source.startsWith('http') && !source.startsWith('git@');
251
+ if (gh && is_shorthand) {
252
+ const validation_error = await validate_github_repo(gh.owner, gh.repo);
253
+ if (validation_error) {
254
+ return { success: false, error: validation_error };
255
+ }
256
+ }
203
257
  try {
204
- await execAsync(`claude plugin marketplace add ${shell_escape(source)} --scope ${scope}`);
258
+ await execAsync(`claude plugin marketplace add ${shell_escape(source)}`);
205
259
  return { success: true };
206
260
  }
207
261
  catch (error) {
208
262
  const message = error instanceof Error ? error.message : 'Unknown error';
263
+ // Provide clearer error messages for common failures
264
+ if (message.includes('SSH') ||
265
+ message.includes('Permission denied (publickey)')) {
266
+ return {
267
+ success: false,
268
+ error: `SSH authentication failed for '${source}'. Either:\n - Configure SSH keys: https://docs.github.com/en/authentication/connecting-to-github-with-ssh\n - Use HTTPS URL instead: https://github.com/${gh ? `${gh.owner}/${gh.repo}` : source}`,
269
+ };
270
+ }
271
+ if (message.includes('not found') ||
272
+ message.includes('does not exist')) {
273
+ return {
274
+ success: false,
275
+ error: `Repository '${source}' not found. Check the name and your access permissions.`,
276
+ };
277
+ }
209
278
  return {
210
279
  success: false,
211
280
  error: `Failed to add marketplace: ${message}`,
@@ -297,6 +366,98 @@ export function get_scope_description(scope) {
297
366
  return 'Global - all projects';
298
367
  }
299
368
  }
369
+ /**
370
+ * Validate a plugin or marketplace manifest via Claude CLI
371
+ */
372
+ export async function validate_plugin_via_cli(path) {
373
+ const cli_available = await check_claude_cli();
374
+ if (!cli_available) {
375
+ return {
376
+ success: false,
377
+ error: 'Claude CLI not found. Please install Claude Code CLI.',
378
+ };
379
+ }
380
+ try {
381
+ const { stdout } = await execAsync(`claude plugin validate ${shell_escape(path)}`);
382
+ return { success: true, stdout: stdout.trim() };
383
+ }
384
+ catch (error) {
385
+ const message = error instanceof Error ? error.message : 'Unknown error';
386
+ return {
387
+ success: false,
388
+ error: `Validation failed: ${message}`,
389
+ };
390
+ }
391
+ }
392
+ /**
393
+ * Get details about an MCP server via Claude CLI
394
+ */
395
+ export async function mcp_get_via_cli(name) {
396
+ const cli_available = await check_claude_cli();
397
+ if (!cli_available) {
398
+ return {
399
+ success: false,
400
+ error: 'Claude CLI not found. Please install Claude Code CLI.',
401
+ };
402
+ }
403
+ try {
404
+ const { stdout } = await execAsync(`claude mcp get ${shell_escape(name)}`);
405
+ return { success: true, stdout: stdout.trim() };
406
+ }
407
+ catch (error) {
408
+ const message = error instanceof Error ? error.message : 'Unknown error';
409
+ return {
410
+ success: false,
411
+ error: `Failed to get server details: ${message}`,
412
+ };
413
+ }
414
+ }
415
+ /**
416
+ * Add an MCP server from raw JSON via Claude CLI
417
+ */
418
+ export async function mcp_add_json_via_cli(name, json, scope = 'local') {
419
+ const cli_available = await check_claude_cli();
420
+ if (!cli_available) {
421
+ return {
422
+ success: false,
423
+ error: 'Claude CLI not found. Please install Claude Code CLI.',
424
+ };
425
+ }
426
+ try {
427
+ await execAsync(`claude mcp add-json ${shell_escape(name)} ${shell_escape(json)} --scope ${scope}`);
428
+ return { success: true };
429
+ }
430
+ catch (error) {
431
+ const message = error instanceof Error ? error.message : 'Unknown error';
432
+ return {
433
+ success: false,
434
+ error: `Failed to add server from JSON: ${message}`,
435
+ };
436
+ }
437
+ }
438
+ /**
439
+ * Reset project-scoped MCP server choices via Claude CLI
440
+ */
441
+ export async function mcp_reset_project_choices_via_cli() {
442
+ const cli_available = await check_claude_cli();
443
+ if (!cli_available) {
444
+ return {
445
+ success: false,
446
+ error: 'Claude CLI not found. Please install Claude Code CLI.',
447
+ };
448
+ }
449
+ try {
450
+ await execAsync('claude mcp reset-project-choices');
451
+ return { success: true };
452
+ }
453
+ catch (error) {
454
+ const message = error instanceof Error ? error.message : 'Unknown error';
455
+ return {
456
+ success: false,
457
+ error: `Failed to reset project choices: ${message}`,
458
+ };
459
+ }
460
+ }
300
461
  /**
301
462
  * Get scope options for select prompt
302
463
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcpick",
3
- "version": "0.0.15",
3
+ "version": "0.0.16",
4
4
  "description": "Dynamic MCP server and plugin configuration manager for Claude Code",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",