mcpick 0.0.7 → 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/CHANGELOG.md +14 -0
- package/README.md +35 -5
- package/dist/commands/add-server.js +55 -8
- package/dist/commands/edit-config.js +71 -9
- package/dist/commands/edit-plugins.js +56 -0
- package/dist/core/config.js +102 -1
- package/dist/core/settings.js +59 -0
- package/dist/index.js +9 -0
- package/dist/utils/claude-cli.js +153 -0
- package/dist/utils/paths.js +21 -0
- package/package.json +4 -4
- package/dist/commands/launch.js +0 -21
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# mcpick
|
|
2
2
|
|
|
3
|
+
## 0.0.9
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- d4f4b5c: Add plugin toggle support - enable/disable Claude Code
|
|
8
|
+
plugins from the mcpick menu
|
|
9
|
+
|
|
10
|
+
## 0.0.8
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- 219d5fd: Add scope support for MCP server installation with Claude
|
|
15
|
+
CLI integration and shell injection fixes
|
|
16
|
+
|
|
3
17
|
## 0.0.7
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -70,7 +70,7 @@ McPick provides an intuitive CLI menu to:
|
|
|
70
70
|
┌ McPick - MCP Server Configuration Manager
|
|
71
71
|
│
|
|
72
72
|
◆ What would you like to do?
|
|
73
|
-
│ ●
|
|
73
|
+
│ ● Enable / Disable MCP servers (Toggle MCP servers on/off)
|
|
74
74
|
│ ○ Backup config
|
|
75
75
|
│ ○ Add MCP server
|
|
76
76
|
│ ○ Restore from backup
|
|
@@ -80,6 +80,26 @@ McPick provides an intuitive CLI menu to:
|
|
|
80
80
|
└
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
+
### Scope Support
|
|
84
|
+
|
|
85
|
+
MCPick supports the three MCP server scopes used by Claude Code:
|
|
86
|
+
|
|
87
|
+
| Scope | Description | Storage Location |
|
|
88
|
+
| ----------- | ------------------------------------ | --------------------------------------------- |
|
|
89
|
+
| **Local** | Project-specific servers (default) | `~/.claude.json` → `projects[cwd].mcpServers` |
|
|
90
|
+
| **Project** | Shared via `.mcp.json` in repository | `.mcp.json` in project root |
|
|
91
|
+
| **User** | Global servers for all projects | `~/.claude.json` → `mcpServers` |
|
|
92
|
+
|
|
93
|
+
When you select "Enable / Disable MCP servers", MCPick will:
|
|
94
|
+
|
|
95
|
+
1. Ask which scope you want to edit
|
|
96
|
+
2. Show servers already enabled for that scope (pre-checked)
|
|
97
|
+
3. Use `claude mcp add/remove` CLI commands for Local and Project
|
|
98
|
+
scopes
|
|
99
|
+
|
|
100
|
+
This integration ensures your changes are correctly applied to the
|
|
101
|
+
right configuration location.
|
|
102
|
+
|
|
83
103
|
### Smart Server Management
|
|
84
104
|
|
|
85
105
|
- **Auto-discovery**: Automatically imports servers from your existing
|
|
@@ -183,15 +203,25 @@ MCPick works with the standard Claude Code configuration format:
|
|
|
183
203
|
|
|
184
204
|
- **Claude Config**: `~/.claude.json` (your main Claude Code
|
|
185
205
|
configuration)
|
|
206
|
+
- **Project Config**: `.mcp.json` (project-specific shared config,
|
|
207
|
+
committed to git)
|
|
186
208
|
- **MCPick Registry**: `~/.claude/mcpick/servers.json` (MCPick's
|
|
187
209
|
server database)
|
|
188
210
|
- **Backups**: `~/.claude/mcpick/backups/` (MCP configuration backups)
|
|
189
211
|
- **Profiles**: `~/.claude/mcpick/profiles/` (predefined server sets)
|
|
190
212
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
213
|
+
#### MCP Server Storage by Scope
|
|
214
|
+
|
|
215
|
+
| Scope | Location | Use Case |
|
|
216
|
+
| ------- | ------------------------------------------------------------ | ---------------------------------- |
|
|
217
|
+
| Local | `~/.claude.json` → `projects["/path/to/project"].mcpServers` | Personal project config |
|
|
218
|
+
| Project | `.mcp.json` in project root | Shared team config (commit to git) |
|
|
219
|
+
| User | `~/.claude.json` → `mcpServers` | Global servers for all projects |
|
|
220
|
+
|
|
221
|
+
> **Note**: MCPick automatically detects servers in parent
|
|
222
|
+
> directories. If you have local servers configured at
|
|
223
|
+
> `/Users/you/projects` and run MCPick from
|
|
224
|
+
> `/Users/you/projects/myapp`, it will find and display them.
|
|
195
225
|
|
|
196
226
|
## Safety Features
|
|
197
227
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { confirm, note, select, text } from '@clack/prompts';
|
|
1
|
+
import { confirm, log, note, select, text } from '@clack/prompts';
|
|
2
2
|
import { add_server_to_registry } from '../core/registry.js';
|
|
3
3
|
import { validate_mcp_server } from '../core/validation.js';
|
|
4
|
+
import { add_mcp_via_cli, check_claude_cli, get_scope_options, get_scope_description, } from '../utils/claude-cli.js';
|
|
4
5
|
function format_server_details(server) {
|
|
5
6
|
const details = [`Name: ${server.name}`];
|
|
6
7
|
if ('command' in server) {
|
|
@@ -23,7 +24,17 @@ function format_server_details(server) {
|
|
|
23
24
|
}
|
|
24
25
|
export async function add_server() {
|
|
25
26
|
try {
|
|
26
|
-
//
|
|
27
|
+
// Check if Claude CLI is available
|
|
28
|
+
const cli_available = await check_claude_cli();
|
|
29
|
+
// First, ask where to install the server (scope)
|
|
30
|
+
const scope = await select({
|
|
31
|
+
message: 'Where should this server be installed?',
|
|
32
|
+
options: get_scope_options(),
|
|
33
|
+
initialValue: 'local',
|
|
34
|
+
});
|
|
35
|
+
if (typeof scope === 'symbol')
|
|
36
|
+
return;
|
|
37
|
+
// Then ask how they want to configure the server
|
|
27
38
|
const config_method = await select({
|
|
28
39
|
message: 'How would you like to add the server?',
|
|
29
40
|
options: [
|
|
@@ -43,7 +54,7 @@ export async function add_server() {
|
|
|
43
54
|
if (typeof config_method === 'symbol')
|
|
44
55
|
return;
|
|
45
56
|
if (config_method === 'json') {
|
|
46
|
-
return await add_server_from_json();
|
|
57
|
+
return await add_server_from_json(scope, cli_available);
|
|
47
58
|
}
|
|
48
59
|
const name = await text({
|
|
49
60
|
message: 'Server name:',
|
|
@@ -181,21 +192,39 @@ export async function add_server() {
|
|
|
181
192
|
}
|
|
182
193
|
const validated_server = validate_mcp_server(server_data);
|
|
183
194
|
const details = format_server_details(validated_server);
|
|
195
|
+
details.push(`Scope: ${get_scope_description(scope)}`);
|
|
184
196
|
note(`Server to add:\n${details.join('\n')}`);
|
|
185
197
|
const should_add = await confirm({
|
|
186
|
-
message: 'Add this server
|
|
198
|
+
message: 'Add this server?',
|
|
187
199
|
});
|
|
188
200
|
if (typeof should_add === 'symbol' || !should_add) {
|
|
189
201
|
return;
|
|
190
202
|
}
|
|
203
|
+
// Always add to registry for profile/backup management
|
|
191
204
|
await add_server_to_registry(validated_server);
|
|
192
|
-
|
|
205
|
+
// Install via Claude CLI if available
|
|
206
|
+
if (cli_available) {
|
|
207
|
+
const result = await add_mcp_via_cli(validated_server, scope);
|
|
208
|
+
if (result.success) {
|
|
209
|
+
note(`Server "${validated_server.name}" installed successfully!\n` +
|
|
210
|
+
`Scope: ${get_scope_description(scope)}\n` +
|
|
211
|
+
`Also added to mcpick registry for profile management.`);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
log.warn(`CLI installation failed: ${result.error}\n` +
|
|
215
|
+
`Server added to registry only. Use 'claude mcp add' manually.`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
log.warn(`Claude CLI not found. Server added to registry only.\n` +
|
|
220
|
+
`Install Claude Code CLI and run 'claude mcp add' to activate.`);
|
|
221
|
+
}
|
|
193
222
|
}
|
|
194
223
|
catch (error) {
|
|
195
224
|
throw new Error(`Failed to add server: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
196
225
|
}
|
|
197
226
|
}
|
|
198
|
-
async function add_server_from_json() {
|
|
227
|
+
async function add_server_from_json(scope, cli_available) {
|
|
199
228
|
const json_input = await text({
|
|
200
229
|
message: 'Paste JSON configuration:',
|
|
201
230
|
placeholder: '{ "name": "mcp-sqlite-tools", "command": "npx", "args": ["-y", "mcp-sqlite-tools"] }',
|
|
@@ -246,15 +275,33 @@ async function add_server_from_json() {
|
|
|
246
275
|
}
|
|
247
276
|
const validated_server = validate_mcp_server(server_data);
|
|
248
277
|
const details = format_server_details(validated_server);
|
|
278
|
+
details.push(`Scope: ${get_scope_description(scope)}`);
|
|
249
279
|
note(`Server to add:\n${details.join('\n')}`);
|
|
250
280
|
const should_add = await confirm({
|
|
251
|
-
message: 'Add this server
|
|
281
|
+
message: 'Add this server?',
|
|
252
282
|
});
|
|
253
283
|
if (typeof should_add === 'symbol' || !should_add) {
|
|
254
284
|
return;
|
|
255
285
|
}
|
|
286
|
+
// Always add to registry for profile/backup management
|
|
256
287
|
await add_server_to_registry(validated_server);
|
|
257
|
-
|
|
288
|
+
// Install via Claude CLI if available
|
|
289
|
+
if (cli_available) {
|
|
290
|
+
const result = await add_mcp_via_cli(validated_server, scope);
|
|
291
|
+
if (result.success) {
|
|
292
|
+
note(`Server "${validated_server.name}" installed successfully!\n` +
|
|
293
|
+
`Scope: ${get_scope_description(scope)}\n` +
|
|
294
|
+
`Also added to mcpick registry for profile management.`);
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
log.warn(`CLI installation failed: ${result.error}\n` +
|
|
298
|
+
`Server added to registry only. Use 'claude mcp add' manually.`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
log.warn(`Claude CLI not found. Server added to registry only.\n` +
|
|
303
|
+
`Install Claude Code CLI and run 'claude mcp add' to activate.`);
|
|
304
|
+
}
|
|
258
305
|
}
|
|
259
306
|
catch (error) {
|
|
260
307
|
throw new Error(`Failed to parse or validate JSON: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
@@ -1,8 +1,19 @@
|
|
|
1
|
-
import { multiselect, note } from '@clack/prompts';
|
|
2
|
-
import { create_config_from_servers, get_enabled_servers, read_claude_config, write_claude_config, } from '../core/config.js';
|
|
1
|
+
import { log, multiselect, note, select } from '@clack/prompts';
|
|
2
|
+
import { create_config_from_servers, get_enabled_servers, get_enabled_servers_for_scope, read_claude_config, write_claude_config, } from '../core/config.js';
|
|
3
3
|
import { get_all_available_servers, sync_servers_to_registry, } from '../core/registry.js';
|
|
4
|
+
import { add_mcp_via_cli, check_claude_cli, get_scope_description, get_scope_options, remove_mcp_via_cli, } from '../utils/claude-cli.js';
|
|
4
5
|
export async function edit_config() {
|
|
5
6
|
try {
|
|
7
|
+
// Check if Claude CLI is available
|
|
8
|
+
const cli_available = await check_claude_cli();
|
|
9
|
+
// Ask which scope to edit
|
|
10
|
+
const scope = await select({
|
|
11
|
+
message: 'Which configuration do you want to edit?',
|
|
12
|
+
options: get_scope_options(),
|
|
13
|
+
initialValue: 'local',
|
|
14
|
+
});
|
|
15
|
+
if (typeof scope === 'symbol')
|
|
16
|
+
return;
|
|
6
17
|
const current_config = await read_claude_config();
|
|
7
18
|
// If registry is empty but .claude.json has servers, populate registry from config
|
|
8
19
|
let all_servers = await get_all_available_servers();
|
|
@@ -18,14 +29,15 @@ export async function edit_config() {
|
|
|
18
29
|
note('No MCP servers found in .claude.json or registry. Add servers first.');
|
|
19
30
|
return;
|
|
20
31
|
}
|
|
21
|
-
|
|
32
|
+
// Get currently enabled servers for the selected scope
|
|
33
|
+
const currently_enabled = await get_enabled_servers_for_scope(scope);
|
|
22
34
|
const server_choices = all_servers.map((server) => ({
|
|
23
35
|
value: server.name,
|
|
24
36
|
label: server.name,
|
|
25
37
|
hint: server.description || '',
|
|
26
38
|
}));
|
|
27
39
|
const selected_server_names = await multiselect({
|
|
28
|
-
message:
|
|
40
|
+
message: `Select MCP servers for ${get_scope_description(scope)}:`,
|
|
29
41
|
options: server_choices,
|
|
30
42
|
initialValues: currently_enabled,
|
|
31
43
|
required: false,
|
|
@@ -34,11 +46,61 @@ export async function edit_config() {
|
|
|
34
46
|
return;
|
|
35
47
|
}
|
|
36
48
|
const selected_servers = all_servers.filter((server) => selected_server_names.includes(server.name));
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
49
|
+
// Determine which servers to add and remove
|
|
50
|
+
const servers_to_add = selected_server_names.filter((name) => !currently_enabled.includes(name));
|
|
51
|
+
const servers_to_remove = currently_enabled.filter((name) => !selected_server_names.includes(name));
|
|
52
|
+
// If CLI is available, use it for add/remove operations
|
|
53
|
+
if (cli_available && (scope === 'local' || scope === 'project')) {
|
|
54
|
+
let success_count = 0;
|
|
55
|
+
let error_count = 0;
|
|
56
|
+
// Add new servers
|
|
57
|
+
for (const name of servers_to_add) {
|
|
58
|
+
const server = all_servers.find((s) => s.name === name);
|
|
59
|
+
if (server) {
|
|
60
|
+
const result = await add_mcp_via_cli(server, scope);
|
|
61
|
+
if (result.success) {
|
|
62
|
+
success_count++;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
error_count++;
|
|
66
|
+
log.warn(`Failed to add ${name}: ${result.error}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Remove servers
|
|
71
|
+
for (const name of servers_to_remove) {
|
|
72
|
+
const result = await remove_mcp_via_cli(name);
|
|
73
|
+
if (result.success) {
|
|
74
|
+
success_count++;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
error_count++;
|
|
78
|
+
log.warn(`Failed to remove ${name}: ${result.error}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
await sync_servers_to_registry(selected_servers);
|
|
82
|
+
if (error_count > 0) {
|
|
83
|
+
note(`Configuration updated with ${error_count} errors.\n` +
|
|
84
|
+
`Scope: ${get_scope_description(scope)}\n` +
|
|
85
|
+
`Added: ${servers_to_add.length}, Removed: ${servers_to_remove.length}`);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
note(`Configuration updated!\n` +
|
|
89
|
+
`Scope: ${get_scope_description(scope)}\n` +
|
|
90
|
+
`Enabled servers: ${selected_servers.length}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
// Fallback to direct file writing (user scope or no CLI)
|
|
95
|
+
const new_config = create_config_from_servers(selected_servers);
|
|
96
|
+
await write_claude_config(new_config);
|
|
97
|
+
await sync_servers_to_registry(selected_servers);
|
|
98
|
+
if (!cli_available && scope !== 'user') {
|
|
99
|
+
log.warn(`Claude CLI not available. Changes written to ~/.claude.json (user scope) instead of ${scope} scope.`);
|
|
100
|
+
}
|
|
101
|
+
note(`Configuration updated!\n` +
|
|
102
|
+
`Enabled servers: ${selected_servers.length}`);
|
|
103
|
+
}
|
|
42
104
|
}
|
|
43
105
|
catch (error) {
|
|
44
106
|
throw new Error(`Failed to edit configuration: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { log, multiselect, note } from '@clack/prompts';
|
|
2
|
+
import { build_enabled_plugins, get_all_plugins, read_claude_settings, write_claude_settings, } from '../core/settings.js';
|
|
3
|
+
export async function edit_plugins() {
|
|
4
|
+
try {
|
|
5
|
+
const settings = await read_claude_settings();
|
|
6
|
+
const plugins = get_all_plugins(settings);
|
|
7
|
+
if (plugins.length === 0) {
|
|
8
|
+
note('No plugins found in ~/.claude/settings.json.\n' +
|
|
9
|
+
'Install plugins via Claude Code: /plugin');
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const plugin_choices = plugins.map((plugin) => ({
|
|
13
|
+
value: `${plugin.name}@${plugin.marketplace}`,
|
|
14
|
+
label: plugin.name,
|
|
15
|
+
hint: plugin.marketplace,
|
|
16
|
+
}));
|
|
17
|
+
const currently_enabled = plugins
|
|
18
|
+
.filter((p) => p.enabled)
|
|
19
|
+
.map((p) => `${p.name}@${p.marketplace}`);
|
|
20
|
+
const selected = await multiselect({
|
|
21
|
+
message: 'Select plugins to enable:',
|
|
22
|
+
options: plugin_choices,
|
|
23
|
+
initialValues: currently_enabled,
|
|
24
|
+
required: false,
|
|
25
|
+
});
|
|
26
|
+
if (typeof selected === 'symbol') {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const selected_set = new Set(selected);
|
|
30
|
+
const updated_plugins = plugins.map((plugin) => ({
|
|
31
|
+
...plugin,
|
|
32
|
+
enabled: selected_set.has(`${plugin.name}@${plugin.marketplace}`),
|
|
33
|
+
}));
|
|
34
|
+
const enabled_plugins = build_enabled_plugins(updated_plugins);
|
|
35
|
+
await write_claude_settings({ enabledPlugins: enabled_plugins });
|
|
36
|
+
const enabled_count = updated_plugins.filter((p) => p.enabled).length;
|
|
37
|
+
const disabled_count = updated_plugins.filter((p) => !p.enabled).length;
|
|
38
|
+
note(`Plugins updated!\n` +
|
|
39
|
+
`Enabled: ${enabled_count}, Disabled: ${disabled_count}`);
|
|
40
|
+
// Show what changed
|
|
41
|
+
const newly_enabled = updated_plugins.filter((p) => p.enabled &&
|
|
42
|
+
!currently_enabled.includes(`${p.name}@${p.marketplace}`));
|
|
43
|
+
const newly_disabled = updated_plugins.filter((p) => !p.enabled &&
|
|
44
|
+
currently_enabled.includes(`${p.name}@${p.marketplace}`));
|
|
45
|
+
if (newly_enabled.length > 0) {
|
|
46
|
+
log.success(`Enabled: ${newly_enabled.map((p) => p.name).join(', ')}`);
|
|
47
|
+
}
|
|
48
|
+
if (newly_disabled.length > 0) {
|
|
49
|
+
log.warn(`Disabled: ${newly_disabled.map((p) => p.name).join(', ')}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
throw new Error(`Failed to edit plugins: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=edit-plugins.js.map
|
package/dist/core/config.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { access, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
-
import { get_claude_config_path } from '../utils/paths.js';
|
|
2
|
+
import { get_claude_config_path, get_current_project_path, get_project_mcp_json_path, } from '../utils/paths.js';
|
|
3
3
|
import { validate_claude_config } from './validation.js';
|
|
4
4
|
export async function read_claude_config() {
|
|
5
5
|
const config_path = get_claude_config_path();
|
|
@@ -51,4 +51,105 @@ export function create_config_from_servers(selected_servers) {
|
|
|
51
51
|
});
|
|
52
52
|
return { mcpServers: mcp_servers };
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Read full Claude config including projects section
|
|
56
|
+
*/
|
|
57
|
+
async function read_claude_config_full() {
|
|
58
|
+
const config_path = get_claude_config_path();
|
|
59
|
+
try {
|
|
60
|
+
await access(config_path);
|
|
61
|
+
const config_content = await readFile(config_path, 'utf-8');
|
|
62
|
+
return JSON.parse(config_content);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
return { mcpServers: {}, projects: {} };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Read MCP servers for local scope (current project)
|
|
70
|
+
* Stored in ~/.claude.json -> projects[cwd].mcpServers
|
|
71
|
+
* Also searches parent directories since Claude CLI may store config at parent level
|
|
72
|
+
*/
|
|
73
|
+
async function read_local_mcp_servers() {
|
|
74
|
+
const { dirname } = await import('node:path');
|
|
75
|
+
const { homedir } = await import('node:os');
|
|
76
|
+
const full_config = await read_claude_config_full();
|
|
77
|
+
const home = homedir();
|
|
78
|
+
let current_dir = get_current_project_path();
|
|
79
|
+
// Search current directory and parents for local config
|
|
80
|
+
while (current_dir &&
|
|
81
|
+
current_dir !== '/' &&
|
|
82
|
+
current_dir.length >= home.length) {
|
|
83
|
+
const project_config = full_config.projects?.[current_dir];
|
|
84
|
+
if (project_config?.mcpServers &&
|
|
85
|
+
Object.keys(project_config.mcpServers).length > 0) {
|
|
86
|
+
return Object.keys(project_config.mcpServers);
|
|
87
|
+
}
|
|
88
|
+
current_dir = dirname(current_dir);
|
|
89
|
+
}
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Read MCP servers from .mcp.json in current project (project scope)
|
|
94
|
+
*/
|
|
95
|
+
async function read_project_mcp_servers() {
|
|
96
|
+
const mcp_json_path = get_project_mcp_json_path();
|
|
97
|
+
try {
|
|
98
|
+
await access(mcp_json_path);
|
|
99
|
+
const content = await readFile(mcp_json_path, 'utf-8');
|
|
100
|
+
const parsed = JSON.parse(content);
|
|
101
|
+
const servers = parsed.mcpServers || {};
|
|
102
|
+
return Object.keys(servers);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Read MCP servers from ~/.claude.json -> mcpServers (user scope)
|
|
110
|
+
*/
|
|
111
|
+
async function read_user_mcp_servers() {
|
|
112
|
+
const config = await read_claude_config();
|
|
113
|
+
return Object.keys(config.mcpServers || {});
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Read MCP servers from .mcp.json files (project scope)
|
|
117
|
+
* Searches current directory and parents for .mcp.json
|
|
118
|
+
*/
|
|
119
|
+
async function find_and_read_project_mcp_json() {
|
|
120
|
+
const { dirname } = await import('node:path');
|
|
121
|
+
let current_dir = get_current_project_path();
|
|
122
|
+
const home = (await import('node:os')).homedir();
|
|
123
|
+
// Search upward for .mcp.json, stop at home or root
|
|
124
|
+
while (current_dir &&
|
|
125
|
+
current_dir !== '/' &&
|
|
126
|
+
current_dir.length >= home.length) {
|
|
127
|
+
const mcp_path = `${current_dir}/.mcp.json`;
|
|
128
|
+
try {
|
|
129
|
+
await access(mcp_path);
|
|
130
|
+
const content = await readFile(mcp_path, 'utf-8');
|
|
131
|
+
const parsed = JSON.parse(content);
|
|
132
|
+
const servers = parsed.mcpServers || {};
|
|
133
|
+
return Object.keys(servers);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// Not found, try parent
|
|
137
|
+
}
|
|
138
|
+
current_dir = dirname(current_dir);
|
|
139
|
+
}
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Get currently enabled server names for a specific scope
|
|
144
|
+
*/
|
|
145
|
+
export async function get_enabled_servers_for_scope(scope) {
|
|
146
|
+
switch (scope) {
|
|
147
|
+
case 'local':
|
|
148
|
+
return read_local_mcp_servers();
|
|
149
|
+
case 'project':
|
|
150
|
+
return find_and_read_project_mcp_json();
|
|
151
|
+
case 'user':
|
|
152
|
+
return read_user_mcp_servers();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
54
155
|
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { access, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { get_claude_settings_path } from '../utils/paths.js';
|
|
3
|
+
export async function read_claude_settings() {
|
|
4
|
+
const settings_path = get_claude_settings_path();
|
|
5
|
+
try {
|
|
6
|
+
await access(settings_path);
|
|
7
|
+
const content = await readFile(settings_path, 'utf-8');
|
|
8
|
+
return JSON.parse(content);
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
if (error instanceof Error &&
|
|
12
|
+
'code' in error &&
|
|
13
|
+
error.code === 'ENOENT') {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export async function write_claude_settings(updates) {
|
|
20
|
+
const settings_path = get_claude_settings_path();
|
|
21
|
+
let existing = {};
|
|
22
|
+
try {
|
|
23
|
+
const content = await readFile(settings_path, 'utf-8');
|
|
24
|
+
existing = JSON.parse(content);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Start with empty if file doesn't exist
|
|
28
|
+
}
|
|
29
|
+
// Merge only the keys we're updating
|
|
30
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
31
|
+
existing[key] = value;
|
|
32
|
+
}
|
|
33
|
+
await writeFile(settings_path, JSON.stringify(existing, null, 2), 'utf-8');
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Parse enabledPlugins into structured list.
|
|
37
|
+
* Keys are in format "plugin-name@marketplace-name"
|
|
38
|
+
*/
|
|
39
|
+
export function get_all_plugins(settings) {
|
|
40
|
+
const enabled_plugins = settings.enabledPlugins || {};
|
|
41
|
+
return Object.entries(enabled_plugins).map(([key, enabled]) => {
|
|
42
|
+
const at_index = key.lastIndexOf('@');
|
|
43
|
+
const name = at_index > 0 ? key.substring(0, at_index) : key;
|
|
44
|
+
const marketplace = at_index > 0 ? key.substring(at_index + 1) : 'unknown';
|
|
45
|
+
return { name, marketplace, enabled };
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Build the enabledPlugins record from a list of PluginInfo
|
|
50
|
+
*/
|
|
51
|
+
export function build_enabled_plugins(plugins) {
|
|
52
|
+
const result = {};
|
|
53
|
+
for (const plugin of plugins) {
|
|
54
|
+
const key = `${plugin.name}@${plugin.marketplace}`;
|
|
55
|
+
result[key] = plugin.enabled;
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=settings.js.map
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { cancel, intro, isCancel, log, outro, select, text, } from '@clack/promp
|
|
|
3
3
|
import { add_server } from './commands/add-server.js';
|
|
4
4
|
import { backup_config } from './commands/backup.js';
|
|
5
5
|
import { edit_config } from './commands/edit-config.js';
|
|
6
|
+
import { edit_plugins } from './commands/edit-plugins.js';
|
|
6
7
|
import { restore_config } from './commands/restore.js';
|
|
7
8
|
import { write_claude_config } from './core/config.js';
|
|
8
9
|
import { list_profiles, load_profile, save_profile, } from './core/profile.js';
|
|
@@ -142,6 +143,11 @@ async function main() {
|
|
|
142
143
|
label: 'Enable / Disable MCP servers',
|
|
143
144
|
hint: 'Toggle MCP servers on/off',
|
|
144
145
|
},
|
|
146
|
+
{
|
|
147
|
+
value: 'edit-plugins',
|
|
148
|
+
label: 'Enable / Disable plugins',
|
|
149
|
+
hint: 'Toggle Claude Code plugins on/off',
|
|
150
|
+
},
|
|
145
151
|
{
|
|
146
152
|
value: 'backup',
|
|
147
153
|
label: 'Backup config',
|
|
@@ -182,6 +188,9 @@ async function main() {
|
|
|
182
188
|
case 'edit-config':
|
|
183
189
|
await edit_config();
|
|
184
190
|
break;
|
|
191
|
+
case 'edit-plugins':
|
|
192
|
+
await edit_plugins();
|
|
193
|
+
break;
|
|
185
194
|
case 'backup':
|
|
186
195
|
await backup_config();
|
|
187
196
|
break;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { exec } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
const execAsync = promisify(exec);
|
|
4
|
+
/**
|
|
5
|
+
* Check if Claude CLI is available
|
|
6
|
+
*/
|
|
7
|
+
export async function check_claude_cli() {
|
|
8
|
+
try {
|
|
9
|
+
await execAsync('claude --version');
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Escape a string for shell usage
|
|
18
|
+
*/
|
|
19
|
+
function shell_escape(str) {
|
|
20
|
+
// Replace single quotes with escaped version
|
|
21
|
+
return `'${str.replace(/'/g, "'\\''")}'`;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Validate environment variable key
|
|
25
|
+
* Must start with letter or underscore, contain only alphanumeric and underscores
|
|
26
|
+
*/
|
|
27
|
+
function is_valid_env_key(key) {
|
|
28
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(key);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Build the claude mcp add command for a server
|
|
32
|
+
*/
|
|
33
|
+
function build_add_command(server, scope) {
|
|
34
|
+
const parts = ['claude', 'mcp', 'add'];
|
|
35
|
+
// Server name
|
|
36
|
+
parts.push(shell_escape(server.name));
|
|
37
|
+
// Transport type
|
|
38
|
+
const transport = server.type || 'stdio';
|
|
39
|
+
parts.push('--transport', transport);
|
|
40
|
+
// Scope
|
|
41
|
+
parts.push('--scope', scope);
|
|
42
|
+
// Handle different transport types
|
|
43
|
+
if (transport === 'stdio') {
|
|
44
|
+
// Environment variables (skip invalid keys to prevent injection)
|
|
45
|
+
if (server.env) {
|
|
46
|
+
for (const [key, value] of Object.entries(server.env)) {
|
|
47
|
+
if (is_valid_env_key(key)) {
|
|
48
|
+
parts.push('-e', `${key}=${shell_escape(value)}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Command and args (after --)
|
|
53
|
+
if ('command' in server && server.command) {
|
|
54
|
+
parts.push('--');
|
|
55
|
+
parts.push(shell_escape(server.command));
|
|
56
|
+
if (server.args && server.args.length > 0) {
|
|
57
|
+
parts.push(...server.args.map((arg) => shell_escape(arg)));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
// HTTP or SSE transport
|
|
63
|
+
if ('url' in server && server.url) {
|
|
64
|
+
parts.push(shell_escape(server.url));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return parts.join(' ');
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Add an MCP server using Claude CLI
|
|
71
|
+
*/
|
|
72
|
+
export async function add_mcp_via_cli(server, scope) {
|
|
73
|
+
// Check if CLI is available
|
|
74
|
+
const cli_available = await check_claude_cli();
|
|
75
|
+
if (!cli_available) {
|
|
76
|
+
return {
|
|
77
|
+
success: false,
|
|
78
|
+
error: 'Claude CLI not found. Please install Claude Code CLI.',
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const command = build_add_command(server, scope);
|
|
82
|
+
try {
|
|
83
|
+
await execAsync(command);
|
|
84
|
+
return { success: true };
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
88
|
+
return {
|
|
89
|
+
success: false,
|
|
90
|
+
error: `Failed to add server via CLI: ${message}`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Remove an MCP server using Claude CLI
|
|
96
|
+
*/
|
|
97
|
+
export async function remove_mcp_via_cli(name) {
|
|
98
|
+
// Check if CLI is available
|
|
99
|
+
const cli_available = await check_claude_cli();
|
|
100
|
+
if (!cli_available) {
|
|
101
|
+
return {
|
|
102
|
+
success: false,
|
|
103
|
+
error: 'Claude CLI not found. Please install Claude Code CLI.',
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
await execAsync(`claude mcp remove ${shell_escape(name)}`);
|
|
108
|
+
return { success: true };
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
112
|
+
return {
|
|
113
|
+
success: false,
|
|
114
|
+
error: `Failed to remove server via CLI: ${message}`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Get the scope description for display
|
|
120
|
+
*/
|
|
121
|
+
export function get_scope_description(scope) {
|
|
122
|
+
switch (scope) {
|
|
123
|
+
case 'local':
|
|
124
|
+
return 'This project only (default)';
|
|
125
|
+
case 'project':
|
|
126
|
+
return 'Shared via .mcp.json (version controlled)';
|
|
127
|
+
case 'user':
|
|
128
|
+
return 'Global - all projects';
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Get scope options for select prompt
|
|
133
|
+
*/
|
|
134
|
+
export function get_scope_options() {
|
|
135
|
+
return [
|
|
136
|
+
{
|
|
137
|
+
value: 'local',
|
|
138
|
+
label: 'Local',
|
|
139
|
+
hint: 'This project only (default)',
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
value: 'project',
|
|
143
|
+
label: 'Project',
|
|
144
|
+
hint: 'Shared via .mcp.json (git)',
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
value: 'user',
|
|
148
|
+
label: 'User (Global)',
|
|
149
|
+
hint: 'Available in all projects',
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=claude-cli.js.map
|
package/dist/utils/paths.js
CHANGED
|
@@ -25,6 +25,9 @@ export function get_claude_config_path() {
|
|
|
25
25
|
}
|
|
26
26
|
return join(parentDir, '.claude.json');
|
|
27
27
|
}
|
|
28
|
+
export function get_claude_settings_path() {
|
|
29
|
+
return join(get_base_dir().baseDir, 'settings.json');
|
|
30
|
+
}
|
|
28
31
|
export function get_mcpick_dir() {
|
|
29
32
|
return join(get_base_dir().baseDir, 'mcpick');
|
|
30
33
|
}
|
|
@@ -60,4 +63,22 @@ export async function ensure_directory_exists(dir_path) {
|
|
|
60
63
|
await mkdir(dir_path, { recursive: true });
|
|
61
64
|
}
|
|
62
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Get the current working directory (project path)
|
|
68
|
+
*/
|
|
69
|
+
export function get_current_project_path() {
|
|
70
|
+
return process.cwd();
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Get the path to .mcp.json in the current project directory (project scope)
|
|
74
|
+
*/
|
|
75
|
+
export function get_project_mcp_json_path() {
|
|
76
|
+
return join(get_current_project_path(), '.mcp.json');
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Get the path to the global .mcp.json in home directory (user scope)
|
|
80
|
+
*/
|
|
81
|
+
export function get_global_mcp_json_path() {
|
|
82
|
+
return join(homedir(), '.mcp.json');
|
|
83
|
+
}
|
|
63
84
|
//# sourceMappingURL=paths.js.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcpick",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Dynamic MCP server configuration manager for Claude Code",
|
|
3
|
+
"version": "0.0.9",
|
|
4
|
+
"description": "Dynamic MCP server and plugin configuration manager for Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"bin": {
|
|
@@ -20,12 +20,12 @@
|
|
|
20
20
|
"author": "",
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@clack/prompts": "^0.
|
|
23
|
+
"@clack/prompts": "^1.0.0",
|
|
24
24
|
"valibot": "^1.2.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@changesets/cli": "^2.29.8",
|
|
28
|
-
"@types/node": "^
|
|
28
|
+
"@types/node": "^25.0.1",
|
|
29
29
|
"prettier": "^3.7.4",
|
|
30
30
|
"typescript": "^5.9.3"
|
|
31
31
|
},
|
package/dist/commands/launch.js
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { outro } from '@clack/prompts';
|
|
2
|
-
import { execSync } from 'node:child_process';
|
|
3
|
-
export async function launch_claude_code() {
|
|
4
|
-
try {
|
|
5
|
-
outro('Launching Claude Code...');
|
|
6
|
-
// Replace the current process with claude using exec
|
|
7
|
-
// This ensures the terminal stays valid and Claude gets full control
|
|
8
|
-
execSync('claude', {
|
|
9
|
-
stdio: 'inherit',
|
|
10
|
-
});
|
|
11
|
-
}
|
|
12
|
-
catch (error) {
|
|
13
|
-
if (error instanceof Error && 'status' in error) {
|
|
14
|
-
// Claude exited normally, just return
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
// Only throw for actual errors (not exit codes)
|
|
18
|
-
throw new Error(`Failed to launch Claude Code: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
//# sourceMappingURL=launch.js.map
|