mcpick 0.0.7 → 0.0.8

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,12 @@
1
1
  # mcpick
2
2
 
3
+ ## 0.0.8
4
+
5
+ ### Patch Changes
6
+
7
+ - 219d5fd: Add scope support for MCP server installation with Claude
8
+ CLI integration and shell injection fixes
9
+
3
10
  ## 0.0.7
4
11
 
5
12
  ### 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
- │ ● Edit config (Toggle MCP servers on/off)
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
- > **Note**: If your MCP servers do not appear in MCPick, ensure they
192
- > are configured at the global level in Claude Code
193
- > (`~/.claude.json`), not at the project level (`.claude.json` in your
194
- > project directory). MCPick manages the global configuration file.
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
- // First, ask how they want to configure the server
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 to the registry?',
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
- note(`Server "${validated_server.name}" added to registry successfully!`);
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 to the registry?',
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
- note(`Server "${validated_server.name}" added to registry successfully!`);
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
- const currently_enabled = Object.keys(current_config.mcpServers || {});
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: 'Select MCP servers to enable (Toggle On/Off servers with spacebar):',
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
- const new_config = create_config_from_servers(selected_servers);
38
- await write_claude_config(new_config);
39
- await sync_servers_to_registry(selected_servers);
40
- note(`Configuration updated!\n` +
41
- `Enabled servers: ${selected_servers.length}`);
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'}`);
@@ -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,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
@@ -60,4 +60,22 @@ export async function ensure_directory_exists(dir_path) {
60
60
  await mkdir(dir_path, { recursive: true });
61
61
  }
62
62
  }
63
+ /**
64
+ * Get the current working directory (project path)
65
+ */
66
+ export function get_current_project_path() {
67
+ return process.cwd();
68
+ }
69
+ /**
70
+ * Get the path to .mcp.json in the current project directory (project scope)
71
+ */
72
+ export function get_project_mcp_json_path() {
73
+ return join(get_current_project_path(), '.mcp.json');
74
+ }
75
+ /**
76
+ * Get the path to the global .mcp.json in home directory (user scope)
77
+ */
78
+ export function get_global_mcp_json_path() {
79
+ return join(homedir(), '.mcp.json');
80
+ }
63
81
  //# sourceMappingURL=paths.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcpick",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "description": "Dynamic MCP server configuration manager for Claude Code",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",