mcpick 0.0.5 → 0.0.7
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 +12 -0
- package/README.md +49 -6
- package/dist/core/profile.js +70 -0
- package/dist/index.js +143 -1
- package/dist/utils/paths.js +32 -3
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -74,6 +74,8 @@ McPick provides an intuitive CLI menu to:
|
|
|
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
|
```
|
|
@@ -98,14 +100,56 @@ McPick provides an intuitive CLI menu to:
|
|
|
98
100
|
- **Easy restoration**: Restore from any previous backup with a simple
|
|
99
101
|
menu
|
|
100
102
|
|
|
103
|
+
### Profiles
|
|
104
|
+
|
|
105
|
+
Load predefined sets of MCP servers instantly:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
# Apply a profile
|
|
109
|
+
mcpick --profile database
|
|
110
|
+
mcpick -p database
|
|
111
|
+
|
|
112
|
+
# Save current config as a profile
|
|
113
|
+
mcpick --save-profile mysetup
|
|
114
|
+
mcpick -s mysetup
|
|
115
|
+
|
|
116
|
+
# List available profiles
|
|
117
|
+
mcpick --list-profiles
|
|
118
|
+
mcpick -l
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Profiles are stored in `~/.claude/mcpick/profiles/`. You can also
|
|
122
|
+
create them manually:
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
// ~/.claude/mcpick/profiles/database.json
|
|
126
|
+
{
|
|
127
|
+
"mcp-sqlite-tools": {
|
|
128
|
+
"type": "stdio",
|
|
129
|
+
"command": "npx",
|
|
130
|
+
"args": ["-y", "mcp-sqlite-tools"]
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Or use full format with `mcpServers` wrapper:
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"mcpServers": {
|
|
140
|
+
"server-name": { ... }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
101
145
|
### Typical Workflow
|
|
102
146
|
|
|
103
|
-
1. **Before a coding session**: Run
|
|
104
|
-
|
|
147
|
+
1. **Before a coding session**: Run `mcpick -p <profile>` or use the
|
|
148
|
+
interactive menu to enable relevant servers
|
|
105
149
|
2. **Launch Claude Code**: Run `claude` to start with your configured
|
|
106
150
|
servers
|
|
107
|
-
3. **Switch contexts**:
|
|
108
|
-
|
|
151
|
+
3. **Switch contexts**: Run `mcpick -p <other-profile>` to quickly
|
|
152
|
+
switch server sets
|
|
109
153
|
|
|
110
154
|
### Adding New Servers
|
|
111
155
|
|
|
@@ -142,6 +186,7 @@ MCPick works with the standard Claude Code configuration format:
|
|
|
142
186
|
- **MCPick Registry**: `~/.claude/mcpick/servers.json` (MCPick's
|
|
143
187
|
server database)
|
|
144
188
|
- **Backups**: `~/.claude/mcpick/backups/` (MCP configuration backups)
|
|
189
|
+
- **Profiles**: `~/.claude/mcpick/profiles/` (predefined server sets)
|
|
145
190
|
|
|
146
191
|
> **Note**: If your MCP servers do not appear in MCPick, ensure they
|
|
147
192
|
> are configured at the global level in Claude Code
|
|
@@ -169,8 +214,6 @@ McPick is actively being developed with new features planned. See the
|
|
|
169
214
|
[claude-code-settings-schema](https://github.com/spences10/claude-code-settings-schema)
|
|
170
215
|
- **Permissions Management** - Interactive tool permission
|
|
171
216
|
configuration with presets (Safe Mode, Dev Mode, Review Mode)
|
|
172
|
-
- **Configuration Profiles** - Save and switch between complete
|
|
173
|
-
configuration snapshots for different workflows
|
|
174
217
|
|
|
175
218
|
Have ideas for other features?
|
|
176
219
|
[Open an issue](https://github.com/spences10/mcpick/issues) or check
|
|
@@ -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);
|
package/dist/utils/paths.js
CHANGED
|
@@ -1,11 +1,32 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
1
2
|
import { access, mkdir } from 'node:fs/promises';
|
|
2
3
|
import { homedir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
export function get_base_dir() {
|
|
6
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR;
|
|
7
|
+
if (configDir && configDir.length > 0 && existsSync(configDir)) {
|
|
8
|
+
return {
|
|
9
|
+
baseDir: configDir,
|
|
10
|
+
parentDir: dirname(configDir),
|
|
11
|
+
coLocatedConfig: true,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
const defaultDir = join(homedir(), '.claude');
|
|
15
|
+
return {
|
|
16
|
+
baseDir: defaultDir,
|
|
17
|
+
parentDir: dirname(defaultDir),
|
|
18
|
+
coLocatedConfig: false,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
4
21
|
export function get_claude_config_path() {
|
|
5
|
-
|
|
22
|
+
const { baseDir, parentDir, coLocatedConfig } = get_base_dir();
|
|
23
|
+
if (coLocatedConfig) {
|
|
24
|
+
return join(baseDir, '.claude.json');
|
|
25
|
+
}
|
|
26
|
+
return join(parentDir, '.claude.json');
|
|
6
27
|
}
|
|
7
28
|
export function get_mcpick_dir() {
|
|
8
|
-
return join(
|
|
29
|
+
return join(get_base_dir().baseDir, 'mcpick');
|
|
9
30
|
}
|
|
10
31
|
export function get_server_registry_path() {
|
|
11
32
|
return join(get_mcpick_dir(), 'servers.json');
|
|
@@ -13,6 +34,14 @@ export function get_server_registry_path() {
|
|
|
13
34
|
export function get_backups_dir() {
|
|
14
35
|
return join(get_mcpick_dir(), 'backups');
|
|
15
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
|
+
}
|
|
16
45
|
export function get_backup_filename() {
|
|
17
46
|
const now = new Date();
|
|
18
47
|
const year = now.getFullYear();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcpick",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "Dynamic MCP server configuration manager for Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -21,12 +21,12 @@
|
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@clack/prompts": "^0.11.0",
|
|
24
|
-
"valibot": "^1.
|
|
24
|
+
"valibot": "^1.2.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"@changesets/cli": "^2.29.
|
|
28
|
-
"@types/node": "^24.
|
|
29
|
-
"prettier": "^3.
|
|
27
|
+
"@changesets/cli": "^2.29.8",
|
|
28
|
+
"@types/node": "^24.10.1",
|
|
29
|
+
"prettier": "^3.7.4",
|
|
30
30
|
"typescript": "^5.9.3"
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|