mcpick 0.0.6 → 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,18 @@
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
+
10
+ ## 0.0.7
11
+
12
+ ### Patch Changes
13
+
14
+ - fb70e88: add profiles feature
15
+
3
16
  ## 0.0.6
4
17
 
5
18
  ### Patch Changes
package/README.md CHANGED
@@ -70,14 +70,36 @@ 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
77
+ │ ○ Load profile
78
+ │ ○ Save profile
77
79
  │ ○ Exit
78
80
 
79
81
  ```
80
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
+
81
103
  ### Smart Server Management
82
104
 
83
105
  - **Auto-discovery**: Automatically imports servers from your existing
@@ -98,14 +120,56 @@ McPick provides an intuitive CLI menu to:
98
120
  - **Easy restoration**: Restore from any previous backup with a simple
99
121
  menu
100
122
 
123
+ ### Profiles
124
+
125
+ Load predefined sets of MCP servers instantly:
126
+
127
+ ```bash
128
+ # Apply a profile
129
+ mcpick --profile database
130
+ mcpick -p database
131
+
132
+ # Save current config as a profile
133
+ mcpick --save-profile mysetup
134
+ mcpick -s mysetup
135
+
136
+ # List available profiles
137
+ mcpick --list-profiles
138
+ mcpick -l
139
+ ```
140
+
141
+ Profiles are stored in `~/.claude/mcpick/profiles/`. You can also
142
+ create them manually:
143
+
144
+ ```json
145
+ // ~/.claude/mcpick/profiles/database.json
146
+ {
147
+ "mcp-sqlite-tools": {
148
+ "type": "stdio",
149
+ "command": "npx",
150
+ "args": ["-y", "mcp-sqlite-tools"]
151
+ }
152
+ }
153
+ ```
154
+
155
+ Or use full format with `mcpServers` wrapper:
156
+
157
+ ```json
158
+ {
159
+ "mcpServers": {
160
+ "server-name": { ... }
161
+ }
162
+ }
163
+ ```
164
+
101
165
  ### Typical Workflow
102
166
 
103
- 1. **Before a coding session**: Run MCPick and enable only relevant
104
- servers (e.g., just database tools for DB work)
167
+ 1. **Before a coding session**: Run `mcpick -p <profile>` or use the
168
+ interactive menu to enable relevant servers
105
169
  2. **Launch Claude Code**: Run `claude` to start with your configured
106
170
  servers
107
- 3. **Switch contexts**: Re-run MCPick to enable different servers for
108
- different tasks
171
+ 3. **Switch contexts**: Run `mcpick -p <other-profile>` to quickly
172
+ switch server sets
109
173
 
110
174
  ### Adding New Servers
111
175
 
@@ -139,14 +203,25 @@ MCPick works with the standard Claude Code configuration format:
139
203
 
140
204
  - **Claude Config**: `~/.claude.json` (your main Claude Code
141
205
  configuration)
206
+ - **Project Config**: `.mcp.json` (project-specific shared config,
207
+ committed to git)
142
208
  - **MCPick Registry**: `~/.claude/mcpick/servers.json` (MCPick's
143
209
  server database)
144
210
  - **Backups**: `~/.claude/mcpick/backups/` (MCP configuration backups)
211
+ - **Profiles**: `~/.claude/mcpick/profiles/` (predefined server sets)
212
+
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 |
145
220
 
146
- > **Note**: If your MCP servers do not appear in MCPick, ensure they
147
- > are configured at the global level in Claude Code
148
- > (`~/.claude.json`), not at the project level (`.claude.json` in your
149
- > project directory). MCPick manages the global configuration file.
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.
150
225
 
151
226
  ## Safety Features
152
227
 
@@ -169,8 +244,6 @@ McPick is actively being developed with new features planned. See the
169
244
  [claude-code-settings-schema](https://github.com/spences10/claude-code-settings-schema)
170
245
  - **Permissions Management** - Interactive tool permission
171
246
  configuration with presets (Safe Mode, Dev Mode, Review Mode)
172
- - **Configuration Profiles** - Save and switch between complete
173
- configuration snapshots for different workflows
174
247
 
175
248
  Have ideas for other features?
176
249
  [Open an issue](https://github.com/spences10/mcpick/issues) or check
@@ -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,70 @@
1
+ import { access, readFile, readdir, writeFile, } from 'node:fs/promises';
2
+ import { ensure_directory_exists, get_profile_path, get_profiles_dir, } from '../utils/paths.js';
3
+ import { read_claude_config } from './config.js';
4
+ import { validate_claude_config } from './validation.js';
5
+ export async function load_profile(name) {
6
+ const profile_path = get_profile_path(name);
7
+ try {
8
+ await access(profile_path);
9
+ const content = await readFile(profile_path, 'utf-8');
10
+ const parsed = JSON.parse(content);
11
+ // Profile can be either full ClaudeConfig format or just mcpServers object
12
+ if (parsed.mcpServers) {
13
+ return validate_claude_config(parsed);
14
+ }
15
+ // If it's just a servers object, wrap it
16
+ return validate_claude_config({ mcpServers: parsed });
17
+ }
18
+ catch (error) {
19
+ if (error instanceof Error &&
20
+ 'code' in error &&
21
+ error.code === 'ENOENT') {
22
+ throw new Error(`Profile '${name}' not found at ${profile_path}`);
23
+ }
24
+ throw error;
25
+ }
26
+ }
27
+ export async function list_profiles() {
28
+ const profiles_dir = get_profiles_dir();
29
+ try {
30
+ await access(profiles_dir);
31
+ const files = await readdir(profiles_dir);
32
+ const json_files = files.filter((f) => f.endsWith('.json'));
33
+ const profiles = [];
34
+ for (const file of json_files) {
35
+ try {
36
+ const path = get_profile_path(file);
37
+ const content = await readFile(path, 'utf-8');
38
+ const parsed = JSON.parse(content);
39
+ const servers = parsed.mcpServers || parsed;
40
+ profiles.push({
41
+ name: file.replace('.json', ''),
42
+ path,
43
+ serverCount: Object.keys(servers).length,
44
+ });
45
+ }
46
+ catch {
47
+ // Skip invalid profiles
48
+ }
49
+ }
50
+ return profiles;
51
+ }
52
+ catch {
53
+ return [];
54
+ }
55
+ }
56
+ export async function save_profile(name) {
57
+ const config = await read_claude_config();
58
+ const servers = config.mcpServers || {};
59
+ const server_count = Object.keys(servers).length;
60
+ if (server_count === 0) {
61
+ throw new Error('No MCP servers configured to save');
62
+ }
63
+ const profiles_dir = get_profiles_dir();
64
+ await ensure_directory_exists(profiles_dir);
65
+ const profile_path = get_profile_path(name);
66
+ const content = JSON.stringify({ mcpServers: servers }, null, 2);
67
+ await writeFile(profile_path, content, 'utf-8');
68
+ return server_count;
69
+ }
70
+ //# sourceMappingURL=profile.js.map
package/dist/index.js CHANGED
@@ -1,10 +1,136 @@
1
1
  #!/usr/bin/env node
2
- import { cancel, intro, isCancel, outro, select, } from '@clack/prompts';
2
+ import { cancel, intro, isCancel, log, outro, select, text, } from '@clack/prompts';
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
6
  import { restore_config } from './commands/restore.js';
7
+ import { write_claude_config } from './core/config.js';
8
+ import { list_profiles, load_profile, save_profile, } from './core/profile.js';
9
+ function parse_args() {
10
+ const args = process.argv.slice(2);
11
+ const result = {};
12
+ for (let i = 0; i < args.length; i++) {
13
+ if (args[i] === '--profile' || args[i] === '-p') {
14
+ result.profile = args[i + 1];
15
+ i++;
16
+ }
17
+ else if (args[i] === '--save-profile' || args[i] === '-s') {
18
+ result.saveProfile = args[i + 1];
19
+ i++;
20
+ }
21
+ else if (args[i] === '--list-profiles' || args[i] === '-l') {
22
+ result.listProfiles = true;
23
+ }
24
+ }
25
+ return result;
26
+ }
27
+ async function apply_profile(name) {
28
+ intro(`MCPick - Loading profile: ${name}`);
29
+ try {
30
+ const profile_config = await load_profile(name);
31
+ await write_claude_config(profile_config);
32
+ const server_count = Object.keys(profile_config.mcpServers || {}).length;
33
+ log.success(`Profile '${name}' applied (${server_count} servers)`);
34
+ outro('Done!');
35
+ }
36
+ catch (error) {
37
+ if (error instanceof Error) {
38
+ cancel(error.message);
39
+ }
40
+ else {
41
+ cancel('Failed to load profile');
42
+ }
43
+ process.exit(1);
44
+ }
45
+ }
46
+ async function show_profiles() {
47
+ intro('MCPick - Available Profiles');
48
+ const profiles = await list_profiles();
49
+ if (profiles.length === 0) {
50
+ log.warn('No profiles found in ~/.claude/mcpick/profiles/');
51
+ log.info('Create .json files there to use profiles');
52
+ }
53
+ else {
54
+ for (const p of profiles) {
55
+ log.info(`${p.name} (${p.serverCount} servers)`);
56
+ }
57
+ }
58
+ outro('');
59
+ }
60
+ async function create_profile(name) {
61
+ intro(`MCPick - Saving profile: ${name}`);
62
+ try {
63
+ const server_count = await save_profile(name);
64
+ log.success(`Profile '${name}' saved (${server_count} servers)`);
65
+ outro('Done!');
66
+ }
67
+ catch (error) {
68
+ if (error instanceof Error) {
69
+ cancel(error.message);
70
+ }
71
+ else {
72
+ cancel('Failed to save profile');
73
+ }
74
+ process.exit(1);
75
+ }
76
+ }
77
+ async function handle_load_profile() {
78
+ const profiles = await list_profiles();
79
+ if (profiles.length === 0) {
80
+ log.warn('No profiles found');
81
+ log.info('Save a profile first or create one in ~/.claude/mcpick/profiles/');
82
+ return;
83
+ }
84
+ const profile_name = await select({
85
+ message: 'Select a profile to load:',
86
+ options: profiles.map((p) => ({
87
+ value: p.name,
88
+ label: p.name,
89
+ hint: `${p.serverCount} servers`,
90
+ })),
91
+ });
92
+ if (isCancel(profile_name))
93
+ return;
94
+ const profile_config = await load_profile(profile_name);
95
+ await write_claude_config(profile_config);
96
+ const server_count = Object.keys(profile_config.mcpServers || {}).length;
97
+ log.success(`Profile '${profile_name}' applied (${server_count} servers)`);
98
+ }
99
+ async function handle_save_profile() {
100
+ const name = await text({
101
+ message: 'Profile name:',
102
+ placeholder: 'e.g. database, web-dev, minimal',
103
+ validate: (value) => {
104
+ if (!value || value.trim().length === 0) {
105
+ return 'Profile name is required';
106
+ }
107
+ if (!/^[\w-]+$/.test(value)) {
108
+ return 'Use only letters, numbers, underscores, hyphens';
109
+ }
110
+ },
111
+ });
112
+ if (isCancel(name))
113
+ return;
114
+ const server_count = await save_profile(name);
115
+ log.success(`Profile '${name}' saved (${server_count} servers)`);
116
+ }
7
117
  async function main() {
118
+ const args = parse_args();
119
+ // Handle --list-profiles
120
+ if (args.listProfiles) {
121
+ await show_profiles();
122
+ return;
123
+ }
124
+ // Handle --save-profile <name>
125
+ if (args.saveProfile) {
126
+ await create_profile(args.saveProfile);
127
+ return;
128
+ }
129
+ // Handle --profile <name>
130
+ if (args.profile) {
131
+ await apply_profile(args.profile);
132
+ return;
133
+ }
8
134
  intro('MCPick - MCP Server Configuration Manager');
9
135
  while (true) {
10
136
  try {
@@ -31,6 +157,16 @@ async function main() {
31
157
  label: 'Restore from backup',
32
158
  hint: 'Restore from a previous backup',
33
159
  },
160
+ {
161
+ value: 'load-profile',
162
+ label: 'Load profile',
163
+ hint: 'Apply a saved profile',
164
+ },
165
+ {
166
+ value: 'save-profile',
167
+ label: 'Save profile',
168
+ hint: 'Save current config as profile',
169
+ },
34
170
  {
35
171
  value: 'exit',
36
172
  label: 'Exit',
@@ -55,6 +191,12 @@ async function main() {
55
191
  case 'restore':
56
192
  await restore_config();
57
193
  break;
194
+ case 'load-profile':
195
+ await handle_load_profile();
196
+ break;
197
+ case 'save-profile':
198
+ await handle_save_profile();
199
+ break;
58
200
  case 'exit':
59
201
  outro('Goodbye!');
60
202
  process.exit(0);
@@ -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
@@ -1,7 +1,7 @@
1
- import { access, mkdir } from 'node:fs/promises';
2
1
  import { existsSync } from 'node:fs';
2
+ import { access, mkdir } from 'node:fs/promises';
3
3
  import { homedir } from 'node:os';
4
- import { join, dirname } from 'node:path';
4
+ import { dirname, join } from 'node:path';
5
5
  export function get_base_dir() {
6
6
  const configDir = process.env.CLAUDE_CONFIG_DIR;
7
7
  if (configDir && configDir.length > 0 && existsSync(configDir)) {
@@ -34,6 +34,14 @@ export function get_server_registry_path() {
34
34
  export function get_backups_dir() {
35
35
  return join(get_mcpick_dir(), 'backups');
36
36
  }
37
+ export function get_profiles_dir() {
38
+ return join(get_mcpick_dir(), 'profiles');
39
+ }
40
+ export function get_profile_path(name) {
41
+ // Allow .json extension or add it
42
+ const filename = name.endsWith('.json') ? name : `${name}.json`;
43
+ return join(get_profiles_dir(), filename);
44
+ }
37
45
  export function get_backup_filename() {
38
46
  const now = new Date();
39
47
  const year = now.getFullYear();
@@ -52,4 +60,22 @@ export async function ensure_directory_exists(dir_path) {
52
60
  await mkdir(dir_path, { recursive: true });
53
61
  }
54
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
+ }
55
81
  //# sourceMappingURL=paths.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcpick",
3
- "version": "0.0.6",
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",