mcpick 0.0.9 → 0.0.11

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,24 @@
1
1
  # mcpick
2
2
 
3
+ ## 0.0.11
4
+
5
+ ### Patch Changes
6
+
7
+ - 7ec3323: feat: add plugin install, uninstall, and update commands
8
+ via Claude CLI
9
+ - 99b2bf3: feat: add plugin install, uninstall, and update to
10
+ interactive menu
11
+ - 9b1c6d7: feat: add plugin support to backup, restore, and profiles
12
+
13
+ ## 0.0.10
14
+
15
+ ### Patch Changes
16
+
17
+ - ffe29b3: Add non-interactive CLI mode with citty for scripting and
18
+ LLM usage
19
+ - fff6c0d: add plugin cache management with staleness detection and
20
+ cleanup
21
+
3
22
  ## 0.0.9
4
23
 
5
24
  ### Patch Changes
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # McPick
2
2
 
3
- A CLI tool for dynamically managing MCP server configurations in
4
- Claude Code. Enable and disable MCP servers on-demand to optimize
5
- context usage and performance.
3
+ A CLI tool for managing MCP servers, plugins, and plugin caches in
4
+ Claude Code. Toggle servers and plugins on/off, manage stale plugin
5
+ caches, and optimise context usage and performance.
6
6
 
7
7
  ## Installation
8
8
 
@@ -51,10 +51,15 @@ This can lead to:
51
51
 
52
52
  ## The Solution
53
53
 
54
- McPick provides an intuitive CLI menu to:
54
+ McPick provides an intuitive CLI menu and non-interactive subcommands
55
+ to:
55
56
 
56
57
  - ✅ **Toggle servers on/off** - Enable only the MCP servers you need
57
58
  for your current task
59
+ - 🔌 **Toggle plugins on/off** - Enable or disable Claude Code
60
+ marketplace plugins
61
+ - 🗑️ **Manage plugin cache** - Detect stale plugins, clear caches,
62
+ clean orphaned versions, refresh marketplaces
58
63
  - 📁 **Manage server registry** - Keep a database of all your
59
64
  available MCP servers
60
65
  - 🔄 **Safe configuration** - Only modifies the `mcpServers` section,
@@ -71,6 +76,8 @@ McPick provides an intuitive CLI menu to:
71
76
 
72
77
  ◆ What would you like to do?
73
78
  │ ● Enable / Disable MCP servers (Toggle MCP servers on/off)
79
+ │ ○ Enable / Disable plugins (Toggle Claude Code plugins on/off)
80
+ │ ○ Manage plugin cache (View, clear, or refresh plugin caches)
74
81
  │ ○ Backup config
75
82
  │ ○ Add MCP server
76
83
  │ ○ Restore from backup
@@ -162,6 +169,104 @@ Or use full format with `mcpServers` wrapper:
162
169
  }
163
170
  ```
164
171
 
172
+ ### Plugin Cache Management
173
+
174
+ Claude Code caches marketplace plugins at `~/.claude/plugins/cache/`.
175
+ When marketplace authors update plugins, your cached versions can go
176
+ stale. McPick detects this and lets you fix it.
177
+
178
+ #### Interactive
179
+
180
+ Select "Manage plugin cache" from the main menu to:
181
+
182
+ - **View cache status** - See all cached plugins with staleness
183
+ indicators (version mismatch, commits behind, orphaned versions)
184
+ - **Clear plugin caches** - Refreshes the marketplace and clears
185
+ selected caches so they rebuild with the latest version
186
+ - **Clean orphaned versions** - Remove old version directories marked
187
+ as orphaned
188
+ - **Refresh marketplaces** - Git pull all marketplace clones to get
189
+ latest plugin listings
190
+
191
+ #### CLI Subcommands
192
+
193
+ ```bash
194
+ # Show cache status for all plugins
195
+ npx mcpick cache status
196
+ npx mcpick cache status --json
197
+
198
+ # Clear a specific plugin cache (refreshes marketplace first)
199
+ npx mcpick cache clear plugin-name@marketplace
200
+ npx mcpick cache clear --all
201
+
202
+ # Remove orphaned version directories
203
+ npx mcpick cache clean-orphaned
204
+
205
+ # Refresh all marketplace clones
206
+ npx mcpick cache refresh
207
+ ```
208
+
209
+ ### Plugin Management
210
+
211
+ Install, update, and toggle Claude Code marketplace plugins:
212
+
213
+ ```bash
214
+ # List all plugins and their status
215
+ npx mcpick plugins list
216
+ npx mcpick plugins list --json
217
+
218
+ # Install/uninstall a plugin (wraps claude plugin CLI)
219
+ npx mcpick plugins install plugin-name@marketplace
220
+ npx mcpick plugins uninstall plugin-name@marketplace
221
+
222
+ # Update a plugin to latest version
223
+ npx mcpick plugins update plugin-name@marketplace
224
+
225
+ # Enable/disable a plugin
226
+ npx mcpick plugins enable plugin-name@marketplace
227
+ npx mcpick plugins disable plugin-name@marketplace
228
+ ```
229
+
230
+ All plugin commands support `--scope` (user, project, local) and
231
+ `--json` flags.
232
+
233
+ ### CLI Subcommands
234
+
235
+ McPick supports both an interactive menu (default) and non-interactive
236
+ CLI subcommands for scripting and LLM tool use:
237
+
238
+ ```bash
239
+ # MCP server management
240
+ npx mcpick list # List servers
241
+ npx mcpick enable <server> # Enable a server
242
+ npx mcpick disable <server> # Disable a server
243
+ npx mcpick add --name <n> ... # Add a server
244
+ npx mcpick remove <server> # Remove a server
245
+
246
+ # Backups and profiles
247
+ npx mcpick backup # Create backup
248
+ npx mcpick restore [file] # Restore from backup
249
+ npx mcpick profile list # List profiles
250
+ npx mcpick profile load <name> # Load a profile
251
+ npx mcpick profile save <name> # Save current config
252
+
253
+ # Plugin management
254
+ npx mcpick plugins list # List plugins
255
+ npx mcpick plugins enable <key> # Enable plugin
256
+ npx mcpick plugins disable <key> # Disable plugin
257
+ npx mcpick plugins install <key> # Install from marketplace
258
+ npx mcpick plugins uninstall <key> # Remove plugin
259
+ npx mcpick plugins update <key> # Update to latest version
260
+
261
+ # Cache management
262
+ npx mcpick cache status # Show staleness info
263
+ npx mcpick cache clear [key] # Clear plugin cache
264
+ npx mcpick cache clean-orphaned # Remove orphaned dirs
265
+ npx mcpick cache refresh # Git pull marketplaces
266
+ ```
267
+
268
+ All subcommands support `--json` for machine-readable output.
269
+
165
270
  ### Typical Workflow
166
271
 
167
272
  1. **Before a coding session**: Run `mcpick -p <profile>` or use the
@@ -209,6 +314,11 @@ MCPick works with the standard Claude Code configuration format:
209
314
  server database)
210
315
  - **Backups**: `~/.claude/mcpick/backups/` (MCP configuration backups)
211
316
  - **Profiles**: `~/.claude/mcpick/profiles/` (predefined server sets)
317
+ - **Plugin Cache**: `~/.claude/plugins/cache/` (cached plugin files)
318
+ - **Installed Plugins**: `~/.claude/plugins/installed_plugins.json`
319
+ (plugin install registry)
320
+ - **Marketplaces**: `~/.claude/plugins/marketplaces/` (marketplace git
321
+ clones)
212
322
 
213
323
  #### MCP Server Storage by Scope
214
324
 
@@ -0,0 +1,135 @@
1
+ import { defineCommand } from 'citty';
2
+ import { add_server_to_registry } from '../../core/registry.js';
3
+ import { validate_mcp_server } from '../../core/validation.js';
4
+ import { add_mcp_via_cli } from '../../utils/claude-cli.js';
5
+ import { error, output } from '../output.js';
6
+ export default defineCommand({
7
+ meta: {
8
+ name: 'add',
9
+ description: 'Add a new MCP server to the registry and enable it',
10
+ },
11
+ args: {
12
+ name: {
13
+ type: 'string',
14
+ description: 'Server name',
15
+ required: true,
16
+ },
17
+ command: {
18
+ type: 'string',
19
+ description: 'Command to run (for stdio transport)',
20
+ },
21
+ args: {
22
+ type: 'string',
23
+ description: 'Comma-separated arguments (e.g. "npx,-y,mcp-sqlite")',
24
+ },
25
+ url: {
26
+ type: 'string',
27
+ description: 'URL (for sse or http transport)',
28
+ },
29
+ type: {
30
+ type: 'string',
31
+ description: 'Transport type: stdio, sse, or http (default: stdio)',
32
+ default: 'stdio',
33
+ },
34
+ env: {
35
+ type: 'string',
36
+ description: 'Environment variables as KEY=val,KEY=val',
37
+ },
38
+ headers: {
39
+ type: 'string',
40
+ description: 'HTTP headers as KEY=val,KEY=val',
41
+ },
42
+ description: {
43
+ type: 'string',
44
+ description: 'Server description',
45
+ },
46
+ scope: {
47
+ type: 'string',
48
+ description: 'Scope: local, project, or user (default: local)',
49
+ default: 'local',
50
+ },
51
+ json: {
52
+ type: 'boolean',
53
+ description: 'Output as JSON',
54
+ default: false,
55
+ },
56
+ },
57
+ async run({ args }) {
58
+ const scope = args.scope;
59
+ if (!['local', 'project', 'user'].includes(scope)) {
60
+ error(`Invalid scope: ${scope}. Use local, project, or user.`);
61
+ }
62
+ const transport = args.type;
63
+ if (!['stdio', 'sse', 'http'].includes(transport)) {
64
+ error(`Invalid type: ${transport}. Use stdio, sse, or http.`);
65
+ }
66
+ // Build server object
67
+ const server_data = {
68
+ name: args.name,
69
+ };
70
+ if (transport === 'stdio') {
71
+ if (!args.command) {
72
+ error('--command is required for stdio transport');
73
+ }
74
+ server_data.command = args.command;
75
+ if (args.args) {
76
+ server_data.args = args.args.split(',');
77
+ }
78
+ }
79
+ else {
80
+ if (!args.url) {
81
+ error(`--url is required for ${transport} transport`);
82
+ }
83
+ server_data.type = transport;
84
+ server_data.url = args.url;
85
+ if (args.headers) {
86
+ server_data.headers = parse_key_value_pairs(args.headers);
87
+ }
88
+ }
89
+ if (args.env) {
90
+ server_data.env = parse_key_value_pairs(args.env);
91
+ }
92
+ if (args.description) {
93
+ server_data.description = args.description;
94
+ }
95
+ // Validate
96
+ let server;
97
+ try {
98
+ server = validate_mcp_server(server_data);
99
+ }
100
+ catch (err) {
101
+ error(`Invalid server config: ${err instanceof Error ? err.message : 'validation failed'}`);
102
+ }
103
+ // Add to registry
104
+ await add_server_to_registry(server);
105
+ // Enable via CLI
106
+ const result = await add_mcp_via_cli(server, scope);
107
+ if (args.json) {
108
+ output({
109
+ added: server.name,
110
+ scope,
111
+ cli: result.success,
112
+ error: result.error,
113
+ }, true);
114
+ }
115
+ else {
116
+ if (result.success) {
117
+ console.log(`Added '${server.name}' and enabled (scope: ${scope})`);
118
+ }
119
+ else {
120
+ console.log(`Added '${server.name}' to registry but CLI failed: ${result.error}`);
121
+ }
122
+ }
123
+ },
124
+ });
125
+ function parse_key_value_pairs(input) {
126
+ const result = {};
127
+ for (const pair of input.split(',')) {
128
+ const eq = pair.indexOf('=');
129
+ if (eq > 0) {
130
+ result[pair.substring(0, eq)] = pair.substring(eq + 1);
131
+ }
132
+ }
133
+ return result;
134
+ }
135
+ //# sourceMappingURL=add.js.map
@@ -0,0 +1,83 @@
1
+ import { defineCommand } from 'citty';
2
+ import { readdir, unlink, writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { read_claude_config } from '../../core/config.js';
5
+ import { read_claude_settings } from '../../core/settings.js';
6
+ import { ensure_directory_exists, get_backup_filename, get_backups_dir, get_plugin_backup_filename, } from '../../utils/paths.js';
7
+ import { output } from '../output.js';
8
+ const MAX_BACKUPS = 10;
9
+ async function cleanup_old_backups(prefix) {
10
+ try {
11
+ const backups_dir = get_backups_dir();
12
+ const files = await readdir(backups_dir);
13
+ const backup_files = files
14
+ .filter((f) => f.startsWith(prefix) && f.endsWith('.json'))
15
+ .sort()
16
+ .reverse();
17
+ if (backup_files.length > MAX_BACKUPS) {
18
+ for (const file of backup_files.slice(MAX_BACKUPS)) {
19
+ await unlink(join(backups_dir, file));
20
+ }
21
+ }
22
+ }
23
+ catch {
24
+ // Cleanup is best-effort
25
+ }
26
+ }
27
+ export default defineCommand({
28
+ meta: {
29
+ name: 'backup',
30
+ description: 'Create a timestamped backup of MCP servers and plugins',
31
+ },
32
+ args: {
33
+ json: {
34
+ type: 'boolean',
35
+ description: 'Output as JSON',
36
+ default: false,
37
+ },
38
+ },
39
+ async run({ args }) {
40
+ const current_config = await read_claude_config();
41
+ const current_settings = await read_claude_settings();
42
+ const backups_dir = get_backups_dir();
43
+ await ensure_directory_exists(backups_dir);
44
+ // Backup MCP servers
45
+ const mcp_filename = get_backup_filename();
46
+ const mcp_path = join(backups_dir, mcp_filename);
47
+ const mcp_backup = {
48
+ mcpServers: current_config.mcpServers || {},
49
+ };
50
+ await writeFile(mcp_path, JSON.stringify(mcp_backup, null, 2), 'utf-8');
51
+ // Backup plugins
52
+ const plugins = current_settings.enabledPlugins || {};
53
+ const plugin_count = Object.keys(plugins).length;
54
+ let plugin_path = null;
55
+ if (plugin_count > 0) {
56
+ const plugin_filename = get_plugin_backup_filename();
57
+ plugin_path = join(backups_dir, plugin_filename);
58
+ const plugin_backup = { enabledPlugins: plugins };
59
+ await writeFile(plugin_path, JSON.stringify(plugin_backup, null, 2), 'utf-8');
60
+ }
61
+ await cleanup_old_backups('mcp-servers-');
62
+ await cleanup_old_backups('plugins-');
63
+ const server_count = Object.keys(current_config.mcpServers || {}).length;
64
+ if (args.json) {
65
+ output({
66
+ mcp: { path: mcp_path, servers: server_count },
67
+ plugins: plugin_path
68
+ ? {
69
+ path: plugin_path,
70
+ plugins: plugin_count,
71
+ }
72
+ : null,
73
+ }, true);
74
+ }
75
+ else {
76
+ console.log(`Backup created: ${mcp_path} (${server_count} servers)`);
77
+ if (plugin_path) {
78
+ console.log(`Plugin backup: ${plugin_path} (${plugin_count} plugins)`);
79
+ }
80
+ }
81
+ },
82
+ });
83
+ //# sourceMappingURL=backup.js.map
@@ -0,0 +1,181 @@
1
+ import { defineCommand } from 'citty';
2
+ import { clean_orphaned_versions, clear_plugin_caches, get_cached_plugins_info, read_installed_plugins, refresh_all_marketplaces, } from '../../core/plugin-cache.js';
3
+ import { error, output } from '../output.js';
4
+ const status = defineCommand({
5
+ meta: {
6
+ name: 'status',
7
+ description: 'Show cached plugins with staleness info',
8
+ },
9
+ args: {
10
+ json: {
11
+ type: 'boolean',
12
+ description: 'Output as JSON',
13
+ default: false,
14
+ },
15
+ },
16
+ async run({ args }) {
17
+ const plugins = await get_cached_plugins_info();
18
+ if (args.json) {
19
+ output(plugins, true);
20
+ return;
21
+ }
22
+ if (plugins.length === 0) {
23
+ console.log('No cached plugins found.');
24
+ return;
25
+ }
26
+ for (const p of plugins) {
27
+ const stale_markers = [];
28
+ if (p.isVersionStale) {
29
+ stale_markers.push(`version: ${p.installedVersion} → ${p.latestVersion}`);
30
+ }
31
+ if (p.isShaStale) {
32
+ stale_markers.push('commits behind');
33
+ }
34
+ if (p.orphanedVersions.length > 0) {
35
+ stale_markers.push(`${p.orphanedVersions.length} orphaned`);
36
+ }
37
+ const status_str = stale_markers.length > 0
38
+ ? ` [stale: ${stale_markers.join(', ')}]`
39
+ : ' [up to date]';
40
+ console.log(`${p.name}@${p.marketplace} v${p.installedVersion}${status_str}`);
41
+ }
42
+ },
43
+ });
44
+ const clear = defineCommand({
45
+ meta: {
46
+ name: 'clear',
47
+ description: 'Clear plugin caches (refreshes marketplace first)',
48
+ },
49
+ args: {
50
+ plugin: {
51
+ type: 'positional',
52
+ description: 'Plugin key (name@marketplace) — omit for all',
53
+ required: false,
54
+ },
55
+ all: {
56
+ type: 'boolean',
57
+ description: 'Clear all plugin caches',
58
+ default: false,
59
+ },
60
+ json: {
61
+ type: 'boolean',
62
+ description: 'Output as JSON',
63
+ default: false,
64
+ },
65
+ },
66
+ async run({ args }) {
67
+ const installed = await read_installed_plugins();
68
+ const all_keys = Object.keys(installed.plugins);
69
+ if (all_keys.length === 0) {
70
+ if (args.json) {
71
+ output({ cleared: [], errors: [] }, true);
72
+ }
73
+ else {
74
+ console.log('No cached plugins to clear.');
75
+ }
76
+ return;
77
+ }
78
+ let keys_to_clear;
79
+ if (args.plugin) {
80
+ if (!installed.plugins[args.plugin]) {
81
+ error(`Plugin '${args.plugin}' not found in cache. Run 'mcpick cache status' to see cached plugins.`);
82
+ }
83
+ keys_to_clear = [args.plugin];
84
+ }
85
+ else if (args.all) {
86
+ keys_to_clear = all_keys;
87
+ }
88
+ else {
89
+ error('Specify a plugin key or use --all. Run "mcpick cache status" to see cached plugins.');
90
+ }
91
+ const result = await clear_plugin_caches(keys_to_clear);
92
+ if (args.json) {
93
+ output(result, true);
94
+ }
95
+ else {
96
+ for (const key of result.cleared) {
97
+ console.log(`Cleared: ${key}`);
98
+ }
99
+ for (const err of result.errors) {
100
+ console.error(`Error: ${err}`);
101
+ }
102
+ if (result.cleared.length > 0) {
103
+ console.log('\nRun /reload-plugins in Claude Code or restart your session.');
104
+ }
105
+ }
106
+ },
107
+ });
108
+ const clean_orphaned = defineCommand({
109
+ meta: {
110
+ name: 'clean-orphaned',
111
+ description: 'Remove orphaned plugin version directories',
112
+ },
113
+ args: {
114
+ json: {
115
+ type: 'boolean',
116
+ description: 'Output as JSON',
117
+ default: false,
118
+ },
119
+ },
120
+ async run({ args }) {
121
+ const result = await clean_orphaned_versions();
122
+ if (args.json) {
123
+ output(result, true);
124
+ }
125
+ else if (result.cleaned === 0) {
126
+ console.log('No orphaned versions found.');
127
+ }
128
+ else {
129
+ for (const p of result.paths) {
130
+ console.log(`Removed: ${p}`);
131
+ }
132
+ console.log(`\nCleaned ${result.cleaned} orphaned version(s).`);
133
+ }
134
+ },
135
+ });
136
+ const refresh = defineCommand({
137
+ meta: {
138
+ name: 'refresh',
139
+ description: 'Refresh all marketplace clones (git pull)',
140
+ },
141
+ args: {
142
+ json: {
143
+ type: 'boolean',
144
+ description: 'Output as JSON',
145
+ default: false,
146
+ },
147
+ },
148
+ async run({ args }) {
149
+ const results = await refresh_all_marketplaces();
150
+ if (args.json) {
151
+ const data = Object.fromEntries(results);
152
+ output(data, true);
153
+ return;
154
+ }
155
+ if (results.size === 0) {
156
+ console.log('No marketplaces configured.');
157
+ return;
158
+ }
159
+ for (const [name, result] of results) {
160
+ if (result.success) {
161
+ console.log(`${name} refreshed`);
162
+ }
163
+ else {
164
+ console.error(`${name} failed: ${result.error}`);
165
+ }
166
+ }
167
+ },
168
+ });
169
+ export default defineCommand({
170
+ meta: {
171
+ name: 'cache',
172
+ description: 'Manage plugin cache',
173
+ },
174
+ subCommands: {
175
+ status,
176
+ clear,
177
+ 'clean-orphaned': clean_orphaned,
178
+ refresh,
179
+ },
180
+ });
181
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1,33 @@
1
+ import { defineCommand } from 'citty';
2
+ import { remove_mcp_via_cli } from '../../utils/claude-cli.js';
3
+ import { error } from '../output.js';
4
+ export default defineCommand({
5
+ meta: {
6
+ name: 'disable',
7
+ description: 'Disable an MCP server',
8
+ },
9
+ args: {
10
+ server: {
11
+ type: 'positional',
12
+ description: 'Server name to disable',
13
+ required: true,
14
+ },
15
+ scope: {
16
+ type: 'string',
17
+ description: 'Scope: local, project, or user (default: local)',
18
+ default: 'local',
19
+ },
20
+ },
21
+ async run({ args }) {
22
+ const scope = args.scope;
23
+ if (!['local', 'project', 'user'].includes(scope)) {
24
+ error(`Invalid scope: ${scope}. Use local, project, or user.`);
25
+ }
26
+ const result = await remove_mcp_via_cli(args.server);
27
+ if (!result.success) {
28
+ error(result.error || 'Failed to disable server');
29
+ }
30
+ console.log(`Disabled '${args.server}' (scope: ${scope})`);
31
+ },
32
+ });
33
+ //# sourceMappingURL=disable.js.map
@@ -0,0 +1,39 @@
1
+ import { defineCommand } from 'citty';
2
+ import { get_all_available_servers } from '../../core/registry.js';
3
+ import { add_mcp_via_cli } from '../../utils/claude-cli.js';
4
+ import { error } from '../output.js';
5
+ export default defineCommand({
6
+ meta: {
7
+ name: 'enable',
8
+ description: 'Enable an MCP server',
9
+ },
10
+ args: {
11
+ server: {
12
+ type: 'positional',
13
+ description: 'Server name to enable',
14
+ required: true,
15
+ },
16
+ scope: {
17
+ type: 'string',
18
+ description: 'Scope: local, project, or user (default: local)',
19
+ default: 'local',
20
+ },
21
+ },
22
+ async run({ args }) {
23
+ const scope = args.scope;
24
+ if (!['local', 'project', 'user'].includes(scope)) {
25
+ error(`Invalid scope: ${scope}. Use local, project, or user.`);
26
+ }
27
+ const all_servers = await get_all_available_servers();
28
+ const server = all_servers.find((s) => s.name === args.server);
29
+ if (!server) {
30
+ error(`Server '${args.server}' not found in registry. Run 'mcpick list' to see available servers.`);
31
+ }
32
+ const result = await add_mcp_via_cli(server, scope);
33
+ if (!result.success) {
34
+ error(result.error || 'Failed to enable server');
35
+ }
36
+ console.log(`Enabled '${server.name}' (scope: ${scope})`);
37
+ },
38
+ });
39
+ //# sourceMappingURL=enable.js.map