mcpick 0.0.9 → 0.0.10

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.10
4
+
5
+ ### Patch Changes
6
+
7
+ - ffe29b3: Add non-interactive CLI mode with citty for scripting and
8
+ LLM usage
9
+ - fff6c0d: add plugin cache management with staleness detection and
10
+ cleanup
11
+
3
12
  ## 0.0.9
4
13
 
5
14
  ### Patch Changes
@@ -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,55 @@
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 { ensure_directory_exists, get_backup_filename, get_backups_dir, } from '../../utils/paths.js';
6
+ import { output } from '../output.js';
7
+ const MAX_BACKUPS = 10;
8
+ export default defineCommand({
9
+ meta: {
10
+ name: 'backup',
11
+ description: 'Create a timestamped backup of MCP server config',
12
+ },
13
+ args: {
14
+ json: {
15
+ type: 'boolean',
16
+ description: 'Output as JSON',
17
+ default: false,
18
+ },
19
+ },
20
+ async run({ args }) {
21
+ const current_config = await read_claude_config();
22
+ const backups_dir = get_backups_dir();
23
+ await ensure_directory_exists(backups_dir);
24
+ const backup_filename = get_backup_filename();
25
+ const backup_path = join(backups_dir, backup_filename);
26
+ const mcp_backup = {
27
+ mcpServers: current_config.mcpServers || {},
28
+ };
29
+ await writeFile(backup_path, JSON.stringify(mcp_backup, null, 2), 'utf-8');
30
+ // Cleanup old backups
31
+ try {
32
+ const files = await readdir(backups_dir);
33
+ const backup_files = files
34
+ .filter((f) => f.startsWith('mcp-servers-') && f.endsWith('.json'))
35
+ .sort()
36
+ .reverse();
37
+ if (backup_files.length > MAX_BACKUPS) {
38
+ for (const file of backup_files.slice(MAX_BACKUPS)) {
39
+ await unlink(join(backups_dir, file));
40
+ }
41
+ }
42
+ }
43
+ catch {
44
+ // Cleanup is best-effort
45
+ }
46
+ const server_count = Object.keys(current_config.mcpServers || {}).length;
47
+ if (args.json) {
48
+ output({ path: backup_path, servers: server_count }, true);
49
+ }
50
+ else {
51
+ console.log(`Backup created: ${backup_path} (${server_count} servers)`);
52
+ }
53
+ },
54
+ });
55
+ //# 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
@@ -0,0 +1,63 @@
1
+ import { defineCommand } from 'citty';
2
+ import { get_enabled_servers_for_scope } from '../../core/config.js';
3
+ import { get_all_available_servers } from '../../core/registry.js';
4
+ import { error, output } from '../output.js';
5
+ export default defineCommand({
6
+ meta: {
7
+ name: 'list',
8
+ description: 'List all MCP servers and their status',
9
+ },
10
+ args: {
11
+ scope: {
12
+ type: 'string',
13
+ description: 'Scope to check: local, project, or user',
14
+ },
15
+ json: {
16
+ type: 'boolean',
17
+ description: 'Output as JSON',
18
+ default: false,
19
+ },
20
+ },
21
+ async run({ args }) {
22
+ const scopes = args.scope
23
+ ? [args.scope]
24
+ : ['local', 'project', 'user'];
25
+ if (args.scope &&
26
+ !['local', 'project', 'user'].includes(args.scope)) {
27
+ error(`Invalid scope: ${args.scope}. Use local, project, or user.`);
28
+ }
29
+ const all_servers = await get_all_available_servers();
30
+ const enabled_by_scope = {};
31
+ for (const scope of scopes) {
32
+ enabled_by_scope[scope] =
33
+ await get_enabled_servers_for_scope(scope);
34
+ }
35
+ if (args.json) {
36
+ const data = all_servers.map((server) => {
37
+ const status = {};
38
+ for (const scope of scopes) {
39
+ status[scope] = enabled_by_scope[scope].includes(server.name);
40
+ }
41
+ const { name, ...rest } = server;
42
+ return { name, ...status, ...rest };
43
+ });
44
+ output(data, true);
45
+ }
46
+ else {
47
+ if (all_servers.length === 0) {
48
+ console.log('No servers in registry.');
49
+ return;
50
+ }
51
+ for (const server of all_servers) {
52
+ const statuses = scopes
53
+ .map((scope) => {
54
+ const enabled = enabled_by_scope[scope].includes(server.name);
55
+ return `${scope}:${enabled ? 'on' : 'off'}`;
56
+ })
57
+ .join(' ');
58
+ console.log(`${server.name} ${statuses}`);
59
+ }
60
+ }
61
+ },
62
+ });
63
+ //# sourceMappingURL=list.js.map
@@ -0,0 +1,93 @@
1
+ import { defineCommand } from 'citty';
2
+ import { build_enabled_plugins, get_all_plugins, read_claude_settings, write_claude_settings, } from '../../core/settings.js';
3
+ import { error, output } from '../output.js';
4
+ const list = defineCommand({
5
+ meta: {
6
+ name: 'list',
7
+ description: 'List all plugins and their status',
8
+ },
9
+ args: {
10
+ json: {
11
+ type: 'boolean',
12
+ description: 'Output as JSON',
13
+ default: false,
14
+ },
15
+ },
16
+ async run({ args }) {
17
+ const settings = await read_claude_settings();
18
+ const plugins = get_all_plugins(settings);
19
+ if (args.json) {
20
+ output(plugins, true);
21
+ }
22
+ else {
23
+ if (plugins.length === 0) {
24
+ console.log('No plugins found.');
25
+ return;
26
+ }
27
+ for (const p of plugins) {
28
+ const status = p.enabled ? 'on' : 'off';
29
+ console.log(`${p.name}@${p.marketplace} ${status}`);
30
+ }
31
+ }
32
+ },
33
+ });
34
+ const enable = defineCommand({
35
+ meta: {
36
+ name: 'enable',
37
+ description: 'Enable a plugin',
38
+ },
39
+ args: {
40
+ plugin: {
41
+ type: 'positional',
42
+ description: 'Plugin key (name@marketplace)',
43
+ required: true,
44
+ },
45
+ },
46
+ async run({ args }) {
47
+ const settings = await read_claude_settings();
48
+ const plugins = get_all_plugins(settings);
49
+ const key = args.plugin;
50
+ const plugin = plugins.find((p) => `${p.name}@${p.marketplace}` === key);
51
+ if (!plugin) {
52
+ error(`Plugin '${key}' not found. Run 'mcpick plugins list' to see available plugins.`);
53
+ }
54
+ plugin.enabled = true;
55
+ const updated = build_enabled_plugins(plugins);
56
+ await write_claude_settings({ enabledPlugins: updated });
57
+ console.log(`Enabled plugin '${key}'`);
58
+ },
59
+ });
60
+ const disable = defineCommand({
61
+ meta: {
62
+ name: 'disable',
63
+ description: 'Disable a plugin',
64
+ },
65
+ args: {
66
+ plugin: {
67
+ type: 'positional',
68
+ description: 'Plugin key (name@marketplace)',
69
+ required: true,
70
+ },
71
+ },
72
+ async run({ args }) {
73
+ const settings = await read_claude_settings();
74
+ const plugins = get_all_plugins(settings);
75
+ const key = args.plugin;
76
+ const plugin = plugins.find((p) => `${p.name}@${p.marketplace}` === key);
77
+ if (!plugin) {
78
+ error(`Plugin '${key}' not found. Run 'mcpick plugins list' to see available plugins.`);
79
+ }
80
+ plugin.enabled = false;
81
+ const updated = build_enabled_plugins(plugins);
82
+ await write_claude_settings({ enabledPlugins: updated });
83
+ console.log(`Disabled plugin '${key}'`);
84
+ },
85
+ });
86
+ export default defineCommand({
87
+ meta: {
88
+ name: 'plugins',
89
+ description: 'Manage Claude Code plugins',
90
+ },
91
+ subCommands: { list, enable, disable },
92
+ });
93
+ //# sourceMappingURL=plugins.js.map