mcpick 0.0.1
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 +7 -0
- package/README.md +143 -0
- package/dist/commands/add-server.js +91 -0
- package/dist/commands/backup.js +54 -0
- package/dist/commands/edit-config.js +47 -0
- package/dist/commands/launch.js +32 -0
- package/dist/commands/restore.js +67 -0
- package/dist/core/config.js +54 -0
- package/dist/core/registry.js +91 -0
- package/dist/core/validation.js +43 -0
- package/dist/index.js +97 -0
- package/dist/types.js +2 -0
- package/dist/utils/paths.js +34 -0
- package/mcpick-plan.md +223 -0
- package/package.json +42 -0
- package/plan.md +102 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# MCPick
|
|
2
|
+
|
|
3
|
+
A CLI tool for dynamically managing MCP server configurations in
|
|
4
|
+
Claude Code. Enable and disable MCP servers on-demand to optimize
|
|
5
|
+
context usage and performance.
|
|
6
|
+
|
|
7
|
+
## The Problem
|
|
8
|
+
|
|
9
|
+
Using the Claude Code `/doctor` command you may see something like
|
|
10
|
+
this if you have many MCP servers configured:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
Context Usage Warnings
|
|
14
|
+
└ ⚠ Large MCP tools context (~66,687 tokens > 25,000)
|
|
15
|
+
└ MCP servers:
|
|
16
|
+
└ mcp-omnisearch-testing: 20 tools (~10,494 tokens)
|
|
17
|
+
└ mcp-omnisearch: 20 tools (~10,454 tokens)
|
|
18
|
+
└ mcp-sqlite-tools-testing: 19 tools (~9,910 tokens)
|
|
19
|
+
└ mcp-sqlite-tools: 19 tools (~9,872 tokens)
|
|
20
|
+
└ playwright: 21 tools (~9,804 tokens)
|
|
21
|
+
└ (7 more servers)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Claude Code loads **all** MCP servers from your `.claude.json` file at
|
|
25
|
+
startup, regardless of whether you need them for your current task.
|
|
26
|
+
This can lead to:
|
|
27
|
+
|
|
28
|
+
- 🐌 Slower Claude Code startup times
|
|
29
|
+
- 💾 High context token usage
|
|
30
|
+
- 🧠 Cognitive overload from too many available tools
|
|
31
|
+
|
|
32
|
+
## The Solution
|
|
33
|
+
|
|
34
|
+
MCPick provides an intuitive CLI menu to:
|
|
35
|
+
|
|
36
|
+
- ✅ **Toggle servers on/off** - Enable only the MCP servers you need
|
|
37
|
+
for your current task
|
|
38
|
+
- 📁 **Manage server registry** - Keep a database of all your
|
|
39
|
+
available MCP servers
|
|
40
|
+
- 🔄 **Safe configuration** - Only modifies the `mcpServers` section,
|
|
41
|
+
preserving other Claude Code settings
|
|
42
|
+
- 💾 **Backup & restore** - Create focused backups of your MCP server
|
|
43
|
+
configurations
|
|
44
|
+
- 🚀 **Quick launch** - Start Claude Code with your optimized
|
|
45
|
+
configuration
|
|
46
|
+
|
|
47
|
+
## Features
|
|
48
|
+
|
|
49
|
+
### Interactive Menu
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
┌ MCPick - MCP Server Configuration Manager
|
|
53
|
+
│
|
|
54
|
+
◆ What would you like to do?
|
|
55
|
+
│ ● Edit config (Toggle MCP servers on/off)
|
|
56
|
+
│ ○ Backup config
|
|
57
|
+
│ ○ Add MCP server
|
|
58
|
+
│ ○ Restore from backup
|
|
59
|
+
│ ○ Launch Claude Code
|
|
60
|
+
│ ○ Exit
|
|
61
|
+
└
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Smart Server Management
|
|
65
|
+
|
|
66
|
+
- **Auto-discovery**: Automatically imports servers from your existing
|
|
67
|
+
`.claude.json`
|
|
68
|
+
- **Registry sync**: Maintains a registry of available servers for
|
|
69
|
+
quick selection
|
|
70
|
+
- **Selective enabling**: Choose exactly which servers to enable via
|
|
71
|
+
multiselect
|
|
72
|
+
- **Configuration safety**: Preserves all non-MCP settings in your
|
|
73
|
+
Claude Code config
|
|
74
|
+
|
|
75
|
+
### Backup System
|
|
76
|
+
|
|
77
|
+
- **Focused backups**: Only backs up MCP server configurations (not
|
|
78
|
+
the entire 30k+ line config)
|
|
79
|
+
- **Automatic cleanup**: Keeps last 10 backups to prevent storage
|
|
80
|
+
bloat
|
|
81
|
+
- **Easy restoration**: Restore from any previous backup with a simple
|
|
82
|
+
menu
|
|
83
|
+
|
|
84
|
+
### Typical Workflow
|
|
85
|
+
|
|
86
|
+
1. **Before a coding session**: Run MCPick and enable only relevant
|
|
87
|
+
servers (e.g., just database tools for DB work)
|
|
88
|
+
2. **Launch Claude Code**: Use MCPick's "Launch Claude Code" option
|
|
89
|
+
3. **Switch contexts**: Re-run MCPick to enable different servers for
|
|
90
|
+
different tasks
|
|
91
|
+
|
|
92
|
+
### Adding New Servers
|
|
93
|
+
|
|
94
|
+
1. Select "Add MCP server"
|
|
95
|
+
2. Provide server details:
|
|
96
|
+
- Name (e.g., "mcp-sqlite-tools")
|
|
97
|
+
- Command (e.g., "npx")
|
|
98
|
+
- Arguments (e.g., "-y", "mcp-sqlite-tools")
|
|
99
|
+
- Description (optional)
|
|
100
|
+
- Environment variables (optional)
|
|
101
|
+
|
|
102
|
+
## Configuration
|
|
103
|
+
|
|
104
|
+
MCPick works with the standard Claude Code configuration format:
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"mcpServers": {
|
|
109
|
+
"server-name": {
|
|
110
|
+
"command": "npx",
|
|
111
|
+
"args": ["-y", "mcp-server-package"],
|
|
112
|
+
"env": {
|
|
113
|
+
"API_KEY": "your-key"
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### File Locations
|
|
121
|
+
|
|
122
|
+
- **Claude Config**: `~/.claude.json` (your main Claude Code
|
|
123
|
+
configuration)
|
|
124
|
+
- **MCPick Registry**: `~/.claude/mcpick/servers.json` (MCPick's
|
|
125
|
+
server database)
|
|
126
|
+
- **Backups**: `~/.claude/mcpick/backups/` (MCP configuration backups)
|
|
127
|
+
|
|
128
|
+
## Safety Features
|
|
129
|
+
|
|
130
|
+
- **Non-destructive**: Only modifies the `mcpServers` section of your
|
|
131
|
+
Claude Code config
|
|
132
|
+
- **Backup integration**: Automatically creates backups before major
|
|
133
|
+
changes
|
|
134
|
+
- **Validation**: Ensures all server configurations are valid before
|
|
135
|
+
writing
|
|
136
|
+
- **Error handling**: Graceful failure modes with helpful error
|
|
137
|
+
messages
|
|
138
|
+
|
|
139
|
+
## Requirements
|
|
140
|
+
|
|
141
|
+
- Node.js 22+
|
|
142
|
+
- Claude Code installed and configured
|
|
143
|
+
- pnpm (for building from source)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { confirm, note, text } from '@clack/prompts';
|
|
2
|
+
import { add_server_to_registry } from '../core/registry.js';
|
|
3
|
+
import { validate_mcp_server } from '../core/validation.js';
|
|
4
|
+
export async function add_server() {
|
|
5
|
+
try {
|
|
6
|
+
const name = await text({
|
|
7
|
+
message: 'Server name:',
|
|
8
|
+
placeholder: 'e.g., mcp-sqlite-tools',
|
|
9
|
+
validate: (value) => {
|
|
10
|
+
if (!value || value.trim().length === 0) {
|
|
11
|
+
return 'Server name is required';
|
|
12
|
+
}
|
|
13
|
+
return undefined;
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
if (typeof name === 'symbol')
|
|
17
|
+
return;
|
|
18
|
+
const command = await text({
|
|
19
|
+
message: 'Command to run:',
|
|
20
|
+
placeholder: 'e.g., uvx, npx, node',
|
|
21
|
+
validate: (value) => {
|
|
22
|
+
if (!value || value.trim().length === 0) {
|
|
23
|
+
return 'Command is required';
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
if (typeof command === 'symbol')
|
|
29
|
+
return;
|
|
30
|
+
const args_input = await text({
|
|
31
|
+
message: 'Arguments (comma-separated):',
|
|
32
|
+
placeholder: 'e.g., mcp-sqlite-tools, --port, 3000',
|
|
33
|
+
defaultValue: '',
|
|
34
|
+
});
|
|
35
|
+
if (typeof args_input === 'symbol')
|
|
36
|
+
return;
|
|
37
|
+
const args = args_input
|
|
38
|
+
.split(',')
|
|
39
|
+
.map((arg) => arg.trim())
|
|
40
|
+
.filter((arg) => arg.length > 0);
|
|
41
|
+
const description = await text({
|
|
42
|
+
message: 'Description (optional):',
|
|
43
|
+
placeholder: 'Brief description of what this server provides',
|
|
44
|
+
});
|
|
45
|
+
if (typeof description === 'symbol')
|
|
46
|
+
return;
|
|
47
|
+
const estimated_tokens_input = await text({
|
|
48
|
+
message: 'Estimated tokens (optional):',
|
|
49
|
+
placeholder: 'e.g., 5000',
|
|
50
|
+
validate: (value) => {
|
|
51
|
+
if (value && value.trim().length > 0) {
|
|
52
|
+
const num = parseInt(value.trim());
|
|
53
|
+
if (isNaN(num) || num < 0) {
|
|
54
|
+
return 'Must be a positive number';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
if (typeof estimated_tokens_input === 'symbol')
|
|
61
|
+
return;
|
|
62
|
+
const server_data = {
|
|
63
|
+
name: name.trim(),
|
|
64
|
+
command: command.trim(),
|
|
65
|
+
args,
|
|
66
|
+
...(description &&
|
|
67
|
+
description.trim() && { description: description.trim() }),
|
|
68
|
+
...(estimated_tokens_input &&
|
|
69
|
+
estimated_tokens_input.trim() && {
|
|
70
|
+
estimated_tokens: parseInt(estimated_tokens_input.trim()),
|
|
71
|
+
}),
|
|
72
|
+
};
|
|
73
|
+
const validated_server = validate_mcp_server(server_data);
|
|
74
|
+
note(`Server to add:\n` +
|
|
75
|
+
`Name: ${validated_server.name}\n` +
|
|
76
|
+
`Command: ${validated_server.command} ${validated_server.args.join(' ')}\n` +
|
|
77
|
+
`Description: ${validated_server.description || 'None'}`);
|
|
78
|
+
const should_add = await confirm({
|
|
79
|
+
message: 'Add this server to the registry?',
|
|
80
|
+
});
|
|
81
|
+
if (typeof should_add === 'symbol' || !should_add) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
await add_server_to_registry(validated_server);
|
|
85
|
+
note(`Server "${validated_server.name}" added to registry successfully!`);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
throw new Error(`Failed to add server: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=add-server.js.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { note } from '@clack/prompts';
|
|
2
|
+
import { readdir, unlink, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { read_claude_config } from '../core/config.js';
|
|
5
|
+
import { ensure_directory_exists, get_backup_filename, get_backups_dir, } from '../utils/paths.js';
|
|
6
|
+
const MAX_BACKUPS = 10;
|
|
7
|
+
export async function backup_config() {
|
|
8
|
+
try {
|
|
9
|
+
const current_config = await read_claude_config();
|
|
10
|
+
const backups_dir = get_backups_dir();
|
|
11
|
+
await ensure_directory_exists(backups_dir);
|
|
12
|
+
const backup_filename = get_backup_filename();
|
|
13
|
+
const backup_path = join(backups_dir, backup_filename);
|
|
14
|
+
const mcp_backup = {
|
|
15
|
+
mcpServers: current_config.mcpServers || {},
|
|
16
|
+
};
|
|
17
|
+
const backup_content = JSON.stringify(mcp_backup, null, 2);
|
|
18
|
+
await writeFile(backup_path, backup_content, 'utf-8');
|
|
19
|
+
await cleanup_old_backups();
|
|
20
|
+
const server_count = Object.keys(current_config.mcpServers || {}).length;
|
|
21
|
+
note(`MCP servers backup created:\n${backup_path}\n(${server_count} servers backed up)`);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
throw new Error(`Failed to create backup: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function cleanup_old_backups() {
|
|
28
|
+
try {
|
|
29
|
+
const backups_dir = get_backups_dir();
|
|
30
|
+
const files = await readdir(backups_dir);
|
|
31
|
+
const backup_files = files
|
|
32
|
+
.filter((file) => file.startsWith('mcp-servers-') && file.endsWith('.json'))
|
|
33
|
+
.map((file) => {
|
|
34
|
+
const timestamp_match = file.match(/mcp-servers-(\d{4})-(\d{2})-(\d{2})-(\d{2})(\d{2})(\d{2})\.json/);
|
|
35
|
+
if (!timestamp_match)
|
|
36
|
+
return null;
|
|
37
|
+
const [, year, month, day, hour, minute, second] = timestamp_match;
|
|
38
|
+
const timestamp = new Date(parseInt(year), parseInt(month) - 1, parseInt(day), parseInt(hour), parseInt(minute), parseInt(second));
|
|
39
|
+
return { file, timestamp };
|
|
40
|
+
})
|
|
41
|
+
.filter((backup) => backup !== null)
|
|
42
|
+
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
|
43
|
+
if (backup_files.length > MAX_BACKUPS) {
|
|
44
|
+
const files_to_delete = backup_files.slice(MAX_BACKUPS);
|
|
45
|
+
for (const { file } of files_to_delete) {
|
|
46
|
+
await unlink(join(backups_dir, file));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
console.warn('Warning: Failed to cleanup old backups:', error);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=backup.js.map
|
|
@@ -0,0 +1,47 @@
|
|
|
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';
|
|
3
|
+
import { get_all_available_servers, sync_servers_to_registry, } from '../core/registry.js';
|
|
4
|
+
export async function edit_config() {
|
|
5
|
+
try {
|
|
6
|
+
const current_config = await read_claude_config();
|
|
7
|
+
// If registry is empty but .claude.json has servers, populate registry from config
|
|
8
|
+
let all_servers = await get_all_available_servers();
|
|
9
|
+
if (all_servers.length === 0 && current_config.mcpServers) {
|
|
10
|
+
const current_servers = get_enabled_servers(current_config);
|
|
11
|
+
if (current_servers.length > 0) {
|
|
12
|
+
await sync_servers_to_registry(current_servers);
|
|
13
|
+
all_servers = current_servers;
|
|
14
|
+
note(`Imported ${current_servers.length} servers from your .claude.json file into registry.`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (all_servers.length === 0) {
|
|
18
|
+
note('No MCP servers found in .claude.json or registry. Add servers first.');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const currently_enabled = Object.keys(current_config.mcpServers || {});
|
|
22
|
+
const server_choices = all_servers.map((server) => ({
|
|
23
|
+
value: server.name,
|
|
24
|
+
label: server.name,
|
|
25
|
+
hint: server.description || '',
|
|
26
|
+
}));
|
|
27
|
+
const selected_server_names = await multiselect({
|
|
28
|
+
message: 'Select MCP servers to enable:',
|
|
29
|
+
options: server_choices,
|
|
30
|
+
initialValues: currently_enabled,
|
|
31
|
+
required: false,
|
|
32
|
+
});
|
|
33
|
+
if (typeof selected_server_names === 'symbol') {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
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}`);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
throw new Error(`Failed to edit configuration: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=edit-config.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { spinner } from '@clack/prompts';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
export async function launch_claude_code() {
|
|
4
|
+
const s = spinner();
|
|
5
|
+
try {
|
|
6
|
+
s.start('Launching Claude Code...');
|
|
7
|
+
const claude_process = spawn('claude', ['code'], {
|
|
8
|
+
stdio: 'inherit',
|
|
9
|
+
detached: true,
|
|
10
|
+
});
|
|
11
|
+
claude_process.unref();
|
|
12
|
+
await new Promise((resolve, reject) => {
|
|
13
|
+
claude_process.on('error', (error) => {
|
|
14
|
+
if (error.message.includes('ENOENT')) {
|
|
15
|
+
reject(new Error('Claude Code not found. Make sure it is installed and in your PATH.'));
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
reject(error);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
setTimeout(() => {
|
|
22
|
+
resolve(void 0);
|
|
23
|
+
}, 1000);
|
|
24
|
+
});
|
|
25
|
+
s.stop('Claude Code launched successfully!');
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
s.stop('Failed to launch Claude Code');
|
|
29
|
+
throw new Error(`Failed to launch Claude Code: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=launch.js.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { confirm, note, select } from '@clack/prompts';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { read_claude_config, write_claude_config, } from '../core/config.js';
|
|
4
|
+
import { list_backups } from '../core/registry.js';
|
|
5
|
+
export async function restore_config() {
|
|
6
|
+
try {
|
|
7
|
+
const backups = await list_backups();
|
|
8
|
+
if (backups.length === 0) {
|
|
9
|
+
note('No backups found.');
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const backup_choices = backups.map((backup) => ({
|
|
13
|
+
value: backup.path,
|
|
14
|
+
label: `${backup.filename} (${backup.timestamp.toLocaleString()})`,
|
|
15
|
+
hint: format_time_ago(backup.timestamp),
|
|
16
|
+
}));
|
|
17
|
+
const selected_backup_path = await select({
|
|
18
|
+
message: 'Select backup to restore:',
|
|
19
|
+
options: backup_choices,
|
|
20
|
+
});
|
|
21
|
+
if (typeof selected_backup_path === 'symbol') {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const should_restore = await confirm({
|
|
25
|
+
message: 'This will replace your current MCP servers configuration. Continue?',
|
|
26
|
+
});
|
|
27
|
+
if (typeof should_restore === 'symbol' || !should_restore) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
// Read the backup file
|
|
31
|
+
const backup_content = await readFile(selected_backup_path, 'utf-8');
|
|
32
|
+
const backup_data = JSON.parse(backup_content);
|
|
33
|
+
// Read current config and merge
|
|
34
|
+
const current_config = await read_claude_config();
|
|
35
|
+
const updated_config = {
|
|
36
|
+
...current_config,
|
|
37
|
+
mcpServers: backup_data.mcpServers || {},
|
|
38
|
+
};
|
|
39
|
+
// Write back only the updated config
|
|
40
|
+
await write_claude_config(updated_config);
|
|
41
|
+
const server_count = Object.keys(backup_data.mcpServers || {}).length;
|
|
42
|
+
note(`MCP servers configuration restored successfully!\n(${server_count} servers restored)`);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
throw new Error(`Failed to restore configuration: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function format_time_ago(date) {
|
|
49
|
+
const now = new Date();
|
|
50
|
+
const diff = now.getTime() - date.getTime();
|
|
51
|
+
const minutes = Math.floor(diff / 60000);
|
|
52
|
+
const hours = Math.floor(minutes / 60);
|
|
53
|
+
const days = Math.floor(hours / 24);
|
|
54
|
+
if (days > 0) {
|
|
55
|
+
return `${days} day${days > 1 ? 's' : ''} ago`;
|
|
56
|
+
}
|
|
57
|
+
else if (hours > 0) {
|
|
58
|
+
return `${hours} hour${hours > 1 ? 's' : ''} ago`;
|
|
59
|
+
}
|
|
60
|
+
else if (minutes > 0) {
|
|
61
|
+
return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
return 'just now';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=restore.js.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { access, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { get_claude_config_path } from '../utils/paths.js';
|
|
3
|
+
import { validate_claude_config } from './validation.js';
|
|
4
|
+
export async function read_claude_config() {
|
|
5
|
+
const config_path = get_claude_config_path();
|
|
6
|
+
try {
|
|
7
|
+
await access(config_path);
|
|
8
|
+
const config_content = await readFile(config_path, 'utf-8');
|
|
9
|
+
const parsed_config = JSON.parse(config_content);
|
|
10
|
+
return validate_claude_config(parsed_config);
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (error instanceof Error &&
|
|
14
|
+
'code' in error &&
|
|
15
|
+
error.code === 'ENOENT') {
|
|
16
|
+
return { mcpServers: {} };
|
|
17
|
+
}
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export async function write_claude_config(config) {
|
|
22
|
+
const config_path = get_claude_config_path();
|
|
23
|
+
// Read the entire existing file to preserve all other sections
|
|
24
|
+
let existing_config = {};
|
|
25
|
+
try {
|
|
26
|
+
const existing_content = await readFile(config_path, 'utf-8');
|
|
27
|
+
existing_config = JSON.parse(existing_content);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
// If file doesn't exist or is invalid, start with empty object
|
|
31
|
+
}
|
|
32
|
+
// Only update the mcpServers section, preserve everything else
|
|
33
|
+
existing_config.mcpServers = config.mcpServers;
|
|
34
|
+
const config_content = JSON.stringify(existing_config, null, 2);
|
|
35
|
+
await writeFile(config_path, config_content, 'utf-8');
|
|
36
|
+
}
|
|
37
|
+
export function get_enabled_servers(config) {
|
|
38
|
+
if (!config.mcpServers) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
return Object.entries(config.mcpServers).map(([name, server]) => ({
|
|
42
|
+
...server,
|
|
43
|
+
name,
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
export function create_config_from_servers(selected_servers) {
|
|
47
|
+
const mcp_servers = {};
|
|
48
|
+
selected_servers.forEach((server) => {
|
|
49
|
+
const { name, ...server_config } = server;
|
|
50
|
+
mcp_servers[name] = server_config;
|
|
51
|
+
});
|
|
52
|
+
return { mcpServers: mcp_servers };
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { access, readdir, readFile, writeFile, } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { ensure_directory_exists, get_backups_dir, get_mcpick_dir, get_server_registry_path, } from '../utils/paths.js';
|
|
4
|
+
import { validate_server_registry } from './validation.js';
|
|
5
|
+
export async function read_server_registry() {
|
|
6
|
+
const registry_path = get_server_registry_path();
|
|
7
|
+
try {
|
|
8
|
+
await access(registry_path);
|
|
9
|
+
const registry_content = await readFile(registry_path, 'utf-8');
|
|
10
|
+
const parsed_registry = JSON.parse(registry_content);
|
|
11
|
+
return validate_server_registry(parsed_registry);
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (error instanceof Error &&
|
|
15
|
+
'code' in error &&
|
|
16
|
+
error.code === 'ENOENT') {
|
|
17
|
+
await ensure_directory_exists(get_mcpick_dir());
|
|
18
|
+
const default_registry = { servers: [] };
|
|
19
|
+
await write_server_registry(default_registry);
|
|
20
|
+
return default_registry;
|
|
21
|
+
}
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export async function write_server_registry(registry) {
|
|
26
|
+
const registry_path = get_server_registry_path();
|
|
27
|
+
await ensure_directory_exists(get_mcpick_dir());
|
|
28
|
+
const registry_content = JSON.stringify(registry, null, 2);
|
|
29
|
+
await writeFile(registry_path, registry_content, 'utf-8');
|
|
30
|
+
}
|
|
31
|
+
export async function add_server_to_registry(server) {
|
|
32
|
+
const registry = await read_server_registry();
|
|
33
|
+
const existing_index = registry.servers.findIndex((s) => s.name === server.name);
|
|
34
|
+
if (existing_index >= 0) {
|
|
35
|
+
registry.servers[existing_index] = server;
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
registry.servers.push(server);
|
|
39
|
+
}
|
|
40
|
+
await write_server_registry(registry);
|
|
41
|
+
}
|
|
42
|
+
export async function get_all_available_servers() {
|
|
43
|
+
const registry = await read_server_registry();
|
|
44
|
+
return registry.servers;
|
|
45
|
+
}
|
|
46
|
+
export async function sync_servers_to_registry(servers) {
|
|
47
|
+
const registry = await read_server_registry();
|
|
48
|
+
servers.forEach((server) => {
|
|
49
|
+
const existing_index = registry.servers.findIndex((s) => s.name === server.name);
|
|
50
|
+
if (existing_index >= 0) {
|
|
51
|
+
registry.servers[existing_index] = server;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
registry.servers.push(server);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
await write_server_registry(registry);
|
|
58
|
+
}
|
|
59
|
+
export async function list_backups() {
|
|
60
|
+
const backups_dir = get_backups_dir();
|
|
61
|
+
try {
|
|
62
|
+
await access(backups_dir);
|
|
63
|
+
const files = await readdir(backups_dir);
|
|
64
|
+
const backup_files = files
|
|
65
|
+
.filter((file) => file.startsWith('mcp-servers-') && file.endsWith('.json'))
|
|
66
|
+
.map((file) => {
|
|
67
|
+
const timestamp_match = file.match(/mcp-servers-(\d{4})-(\d{2})-(\d{2})-(\d{2})(\d{2})(\d{2})\.json/);
|
|
68
|
+
if (!timestamp_match)
|
|
69
|
+
return null;
|
|
70
|
+
const [, year, month, day, hour, minute, second] = timestamp_match;
|
|
71
|
+
const timestamp = new Date(parseInt(year), parseInt(month) - 1, parseInt(day), parseInt(hour), parseInt(minute), parseInt(second));
|
|
72
|
+
return {
|
|
73
|
+
filename: file,
|
|
74
|
+
timestamp,
|
|
75
|
+
path: join(backups_dir, file),
|
|
76
|
+
};
|
|
77
|
+
})
|
|
78
|
+
.filter((backup) => backup !== null)
|
|
79
|
+
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
|
80
|
+
return backup_files;
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (error instanceof Error &&
|
|
84
|
+
'code' in error &&
|
|
85
|
+
error.code === 'ENOENT') {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as v from 'valibot';
|
|
2
|
+
export const mcp_server_schema = v.object({
|
|
3
|
+
name: v.pipe(v.string(), v.minLength(1)),
|
|
4
|
+
type: v.optional(v.union([
|
|
5
|
+
v.literal('stdio'),
|
|
6
|
+
v.literal('sse'),
|
|
7
|
+
v.literal('http'),
|
|
8
|
+
])),
|
|
9
|
+
command: v.pipe(v.string(), v.minLength(1)),
|
|
10
|
+
args: v.array(v.string()),
|
|
11
|
+
env: v.optional(v.record(v.string(), v.string())),
|
|
12
|
+
url: v.optional(v.string()),
|
|
13
|
+
headers: v.optional(v.record(v.string(), v.string())),
|
|
14
|
+
description: v.optional(v.string()),
|
|
15
|
+
});
|
|
16
|
+
export const claude_config_schema = v.object({
|
|
17
|
+
mcpServers: v.optional(v.record(v.string(), v.object({
|
|
18
|
+
type: v.optional(v.union([
|
|
19
|
+
v.literal('stdio'),
|
|
20
|
+
v.literal('sse'),
|
|
21
|
+
v.literal('http'),
|
|
22
|
+
])),
|
|
23
|
+
command: v.pipe(v.string(), v.minLength(1)),
|
|
24
|
+
args: v.array(v.string()),
|
|
25
|
+
env: v.optional(v.record(v.string(), v.string())),
|
|
26
|
+
url: v.optional(v.string()),
|
|
27
|
+
headers: v.optional(v.record(v.string(), v.string())),
|
|
28
|
+
description: v.optional(v.string()),
|
|
29
|
+
}))),
|
|
30
|
+
});
|
|
31
|
+
export const server_registry_schema = v.object({
|
|
32
|
+
servers: v.array(mcp_server_schema),
|
|
33
|
+
});
|
|
34
|
+
export function validate_mcp_server(data) {
|
|
35
|
+
return v.parse(mcp_server_schema, data);
|
|
36
|
+
}
|
|
37
|
+
export function validate_claude_config(data) {
|
|
38
|
+
return v.parse(claude_config_schema, data);
|
|
39
|
+
}
|
|
40
|
+
export function validate_server_registry(data) {
|
|
41
|
+
return v.parse(server_registry_schema, data);
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=validation.js.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { cancel, intro, isCancel, outro, select, } from '@clack/prompts';
|
|
3
|
+
import { add_server } from './commands/add-server.js';
|
|
4
|
+
import { backup_config } from './commands/backup.js';
|
|
5
|
+
import { edit_config } from './commands/edit-config.js';
|
|
6
|
+
import { launch_claude_code } from './commands/launch.js';
|
|
7
|
+
import { restore_config } from './commands/restore.js';
|
|
8
|
+
async function main() {
|
|
9
|
+
intro('MCPick - MCP Server Configuration Manager');
|
|
10
|
+
while (true) {
|
|
11
|
+
try {
|
|
12
|
+
const action = await select({
|
|
13
|
+
message: 'What would you like to do?',
|
|
14
|
+
options: [
|
|
15
|
+
{
|
|
16
|
+
value: 'edit-config',
|
|
17
|
+
label: 'Edit config',
|
|
18
|
+
hint: 'Toggle MCP servers on/off',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
value: 'backup',
|
|
22
|
+
label: 'Backup config',
|
|
23
|
+
hint: 'Create a timestamped backup',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
value: 'add-server',
|
|
27
|
+
label: 'Add MCP server',
|
|
28
|
+
hint: 'Register a new MCP server',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
value: 'restore',
|
|
32
|
+
label: 'Restore from backup',
|
|
33
|
+
hint: 'Restore from a previous backup',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
value: 'launch',
|
|
37
|
+
label: 'Launch Claude Code',
|
|
38
|
+
hint: 'Start Claude Code with current config',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
value: 'exit',
|
|
42
|
+
label: 'Exit',
|
|
43
|
+
hint: 'Quit MCPick',
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
});
|
|
47
|
+
if (isCancel(action)) {
|
|
48
|
+
cancel('Operation cancelled');
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
switch (action) {
|
|
52
|
+
case 'edit-config':
|
|
53
|
+
await edit_config();
|
|
54
|
+
break;
|
|
55
|
+
case 'backup':
|
|
56
|
+
await backup_config();
|
|
57
|
+
break;
|
|
58
|
+
case 'add-server':
|
|
59
|
+
await add_server();
|
|
60
|
+
break;
|
|
61
|
+
case 'restore':
|
|
62
|
+
await restore_config();
|
|
63
|
+
break;
|
|
64
|
+
case 'launch':
|
|
65
|
+
await launch_claude_code();
|
|
66
|
+
break;
|
|
67
|
+
case 'exit':
|
|
68
|
+
outro('Goodbye!');
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (error instanceof Error) {
|
|
74
|
+
cancel(error.message);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
cancel('An unexpected error occurred');
|
|
78
|
+
}
|
|
79
|
+
const should_continue = await select({
|
|
80
|
+
message: 'Would you like to continue?',
|
|
81
|
+
options: [
|
|
82
|
+
{ value: true, label: 'Yes, return to main menu' },
|
|
83
|
+
{ value: false, label: 'No, exit' },
|
|
84
|
+
],
|
|
85
|
+
});
|
|
86
|
+
if (isCancel(should_continue) || !should_continue) {
|
|
87
|
+
outro('Goodbye!');
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
main().catch((error) => {
|
|
94
|
+
console.error('Fatal error:', error);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
});
|
|
97
|
+
//# sourceMappingURL=index.js.map
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { access, mkdir } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
export function get_claude_config_path() {
|
|
5
|
+
return join(homedir(), '.claude.json');
|
|
6
|
+
}
|
|
7
|
+
export function get_mcpick_dir() {
|
|
8
|
+
return join(homedir(), '.claude', 'mcpick');
|
|
9
|
+
}
|
|
10
|
+
export function get_server_registry_path() {
|
|
11
|
+
return join(get_mcpick_dir(), 'servers.json');
|
|
12
|
+
}
|
|
13
|
+
export function get_backups_dir() {
|
|
14
|
+
return join(get_mcpick_dir(), 'backups');
|
|
15
|
+
}
|
|
16
|
+
export function get_backup_filename() {
|
|
17
|
+
const now = new Date();
|
|
18
|
+
const year = now.getFullYear();
|
|
19
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
20
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
21
|
+
const hour = String(now.getHours()).padStart(2, '0');
|
|
22
|
+
const minute = String(now.getMinutes()).padStart(2, '0');
|
|
23
|
+
const second = String(now.getSeconds()).padStart(2, '0');
|
|
24
|
+
return `mcp-servers-${year}-${month}-${day}-${hour}${minute}${second}.json`;
|
|
25
|
+
}
|
|
26
|
+
export async function ensure_directory_exists(dir_path) {
|
|
27
|
+
try {
|
|
28
|
+
await access(dir_path);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
await mkdir(dir_path, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=paths.js.map
|
package/mcpick-plan.md
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# MCPick Implementation Plan
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Build a minimal Node.js CLI tool using TypeScript and Clack prompts to
|
|
6
|
+
dynamically manage MCP server configurations for Claude Code sessions.
|
|
7
|
+
|
|
8
|
+
## The Problem
|
|
9
|
+
|
|
10
|
+
Claude Code loads ALL configured MCP servers at session startup,
|
|
11
|
+
consuming massive amounts of context tokens (66,687+ tokens reported)
|
|
12
|
+
regardless of whether you actually use those tools. Users need a way
|
|
13
|
+
to dynamically select which MCP servers to load per session.
|
|
14
|
+
|
|
15
|
+
## Architecture - Minimal Dependencies
|
|
16
|
+
|
|
17
|
+
**Only 2 Dependencies:**
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"@clack/prompts": "^0.7.0",
|
|
22
|
+
"valibot": "^0.25.0"
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Use Node.js Built-ins:**
|
|
27
|
+
|
|
28
|
+
- `fs/promises` for all file operations
|
|
29
|
+
- `path`, `os.homedir()` for path handling
|
|
30
|
+
- `child_process.spawn` to launch Claude Code
|
|
31
|
+
- `JSON.parse/stringify` for config files
|
|
32
|
+
|
|
33
|
+
## TypeScript Configuration
|
|
34
|
+
|
|
35
|
+
**tsconfig.json:**
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"compilerOptions": {
|
|
40
|
+
"target": "ES2022",
|
|
41
|
+
"module": "ESNext",
|
|
42
|
+
"moduleResolution": "node16",
|
|
43
|
+
"strict": true,
|
|
44
|
+
"outDir": "./dist",
|
|
45
|
+
"sourceMap": true,
|
|
46
|
+
"esModuleInterop": true,
|
|
47
|
+
"allowSyntheticDefaultImports": true,
|
|
48
|
+
"skipLibCheck": true
|
|
49
|
+
},
|
|
50
|
+
"include": ["src/**/*"],
|
|
51
|
+
"exclude": ["node_modules", "dist"]
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**package.json additions:**
|
|
56
|
+
|
|
57
|
+
- `"type": "module"` for ESM
|
|
58
|
+
- `"bin": { "mcpick": "./dist/index.js" }`
|
|
59
|
+
- `"engines": { "node": ">=22.0.0" }`
|
|
60
|
+
|
|
61
|
+
## User Flow - Pure Interactive
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
mcpick # Single entry point - no CLI arguments
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Main Menu (Clack select):**
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
┌ What would you like to do?
|
|
71
|
+
│ ○ Edit config
|
|
72
|
+
│ ○ Backup config
|
|
73
|
+
│ ○ Add MCP server
|
|
74
|
+
│ ○ Restore from backup
|
|
75
|
+
│ ○ Launch Claude Code
|
|
76
|
+
└ ○ Exit
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Edit Config Flow:
|
|
80
|
+
|
|
81
|
+
1. Read `.claude.json` from current directory
|
|
82
|
+
2. Show multiselect with all servers (currently enabled ones checked)
|
|
83
|
+
3. User toggles servers on/off with spacebar
|
|
84
|
+
4. Save deselected servers to `~/.claude/mcpick/servers.json` registry
|
|
85
|
+
5. Update `.claude.json` with only selected servers
|
|
86
|
+
6. Show token count reduction
|
|
87
|
+
|
|
88
|
+
### Backup Config Flow:
|
|
89
|
+
|
|
90
|
+
1. Create timestamped backup of current `.claude.json`
|
|
91
|
+
2. Store in `~/.claude/mcpick/backups/`
|
|
92
|
+
3. Confirm backup location to user
|
|
93
|
+
|
|
94
|
+
### Add MCP Server Flow:
|
|
95
|
+
|
|
96
|
+
1. Text prompts for server details:
|
|
97
|
+
- Name
|
|
98
|
+
- Command
|
|
99
|
+
- Arguments (array)
|
|
100
|
+
- Description (optional)
|
|
101
|
+
2. Validate configuration with Valibot
|
|
102
|
+
3. Add to both `.claude.json` and servers registry
|
|
103
|
+
|
|
104
|
+
### Restore Flow:
|
|
105
|
+
|
|
106
|
+
1. List available backups with timestamps
|
|
107
|
+
2. User selects backup to restore
|
|
108
|
+
3. Confirm destructive operation
|
|
109
|
+
4. Restore `.claude.json` from backup
|
|
110
|
+
|
|
111
|
+
## File Structure
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
mcpick/
|
|
115
|
+
├── src/
|
|
116
|
+
│ ├── commands/
|
|
117
|
+
│ │ ├── edit-config.ts # Toggle servers on/off
|
|
118
|
+
│ │ ├── backup.ts # Create config backups
|
|
119
|
+
│ │ ├── add-server.ts # Add new MCP server
|
|
120
|
+
│ │ ├── restore.ts # Restore from backup
|
|
121
|
+
│ │ └── launch.ts # Launch Claude Code
|
|
122
|
+
│ ├── core/
|
|
123
|
+
│ │ ├── config.ts # .claude.json read/write operations
|
|
124
|
+
│ │ ├── registry.ts # servers.json management
|
|
125
|
+
│ │ └── validation.ts # Valibot schemas
|
|
126
|
+
│ ├── utils/
|
|
127
|
+
│ │ └── paths.ts # Path resolution utilities
|
|
128
|
+
│ ├── types.ts # TypeScript type definitions
|
|
129
|
+
│ └── index.ts # Main entry point with menu
|
|
130
|
+
├── package.json
|
|
131
|
+
├── tsconfig.json
|
|
132
|
+
└── README.md
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Configuration Storage
|
|
136
|
+
|
|
137
|
+
**Current directory: `.claude.json`**
|
|
138
|
+
|
|
139
|
+
- Standard Claude Code configuration file
|
|
140
|
+
- Only contains currently selected MCP servers
|
|
141
|
+
|
|
142
|
+
**~/.claude/mcpick/servers.json** - Registry of all available servers:
|
|
143
|
+
|
|
144
|
+
```json
|
|
145
|
+
{
|
|
146
|
+
"servers": [
|
|
147
|
+
{
|
|
148
|
+
"name": "mcp-sqlite-tools",
|
|
149
|
+
"command": "uvx",
|
|
150
|
+
"args": ["mcp-sqlite-tools"],
|
|
151
|
+
"description": "SQLite database tools",
|
|
152
|
+
"estimatedTokens": 9872
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"name": "mcp-omnisearch",
|
|
156
|
+
"command": "npx",
|
|
157
|
+
"args": ["-y", "@modelcontextprotocol/server-omnisearch"],
|
|
158
|
+
"description": "Web search capabilities",
|
|
159
|
+
"estimatedTokens": 10454
|
|
160
|
+
}
|
|
161
|
+
]
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**~/.claude/mcpick/backups/** - Timestamped backup files:
|
|
166
|
+
|
|
167
|
+
- Format: `claude-YYYY-MM-DD-HHMMSS.json`
|
|
168
|
+
- Keep last 10 backups automatically
|
|
169
|
+
|
|
170
|
+
## Error Handling Strategy
|
|
171
|
+
|
|
172
|
+
- **Valibot schemas** for all configuration validation
|
|
173
|
+
- **Try/catch blocks** with Clack `cancel()` for user-friendly error
|
|
174
|
+
messages
|
|
175
|
+
- **File permission checks** before attempting operations
|
|
176
|
+
- **Create missing directories/files** with sensible defaults
|
|
177
|
+
- **Graceful handling** of malformed JSON files
|
|
178
|
+
|
|
179
|
+
## Token Estimation
|
|
180
|
+
|
|
181
|
+
- Static analysis of MCP server tool definitions
|
|
182
|
+
- Cache estimates in servers.json to avoid repeated calculations
|
|
183
|
+
- Display real-time totals during server selection
|
|
184
|
+
- Show before/after token counts
|
|
185
|
+
|
|
186
|
+
## Implementation Steps
|
|
187
|
+
|
|
188
|
+
1. **Project Setup**
|
|
189
|
+
- Initialize Node.js/TypeScript project with minimal dependencies
|
|
190
|
+
- Configure ESM, TypeScript, and build scripts
|
|
191
|
+
|
|
192
|
+
2. **Core Infrastructure**
|
|
193
|
+
- Implement config file I/O using only Node.js built-ins
|
|
194
|
+
- Create server registry management
|
|
195
|
+
- Build backup/restore functionality
|
|
196
|
+
|
|
197
|
+
3. **Clack Interface**
|
|
198
|
+
- Main interactive menu system
|
|
199
|
+
- Multiselect for server toggling
|
|
200
|
+
- Text prompts for server addition
|
|
201
|
+
|
|
202
|
+
4. **Config Management**
|
|
203
|
+
- Edit existing configurations
|
|
204
|
+
- Add new MCP servers
|
|
205
|
+
- Backup and restore operations
|
|
206
|
+
|
|
207
|
+
5. **Polish & Testing**
|
|
208
|
+
- Error handling and validation
|
|
209
|
+
- User experience improvements
|
|
210
|
+
- Documentation
|
|
211
|
+
|
|
212
|
+
## Benefits
|
|
213
|
+
|
|
214
|
+
- **95% token reduction** by loading only needed tools
|
|
215
|
+
- **Zero session restarts** for MCP configuration changes
|
|
216
|
+
- **Intuitive interface** with beautiful Clack prompts
|
|
217
|
+
- **Minimal dependencies** - only 2 external packages
|
|
218
|
+
- **Fast startup** with lightweight codebase
|
|
219
|
+
- **Backup safety** for configuration changes
|
|
220
|
+
|
|
221
|
+
This approach transforms MCP configuration from a static, manual
|
|
222
|
+
process into a dynamic, user-friendly workflow that maximizes both
|
|
223
|
+
functionality and efficiency.
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mcpick",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Dynamic MCP server configuration manager for Claude Code",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mcpick": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=22.0.0"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"claude",
|
|
15
|
+
"mcp",
|
|
16
|
+
"cli",
|
|
17
|
+
"configuration",
|
|
18
|
+
"claude-code"
|
|
19
|
+
],
|
|
20
|
+
"author": "",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@clack/prompts": "^0.11.0",
|
|
24
|
+
"valibot": "^1.1.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@changesets/cli": "^2.29.7",
|
|
28
|
+
"@types/node": "^24.5.2",
|
|
29
|
+
"prettier": "^3.6.2",
|
|
30
|
+
"typescript": "^5.9.2"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsc",
|
|
34
|
+
"dev": "tsc --watch",
|
|
35
|
+
"start": "node ./dist/index.js",
|
|
36
|
+
"format": "prettier --write .",
|
|
37
|
+
"format:check": "prettier --check .",
|
|
38
|
+
"changeset": "changeset",
|
|
39
|
+
"version": "changeset version",
|
|
40
|
+
"release": "pnpm run build && changeset publish"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/plan.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# MCP Session Manager: Dynamic Tool Configuration for Claude Code
|
|
2
|
+
|
|
3
|
+
## The Problem
|
|
4
|
+
|
|
5
|
+
Claude Code loads ALL configured MCP servers at session startup,
|
|
6
|
+
consuming massive amounts of context tokens regardless of whether you
|
|
7
|
+
actually use those tools. Users report:
|
|
8
|
+
|
|
9
|
+
- **66,687 tokens consumed** by 20+ MCP tools before even starting
|
|
10
|
+
work
|
|
11
|
+
- **25-30% of context window** used by unused tool definitions
|
|
12
|
+
- **No way to dynamically enable/disable** MCP servers during sessions
|
|
13
|
+
- **Session restart required** for any MCP configuration changes
|
|
14
|
+
- **Forced to choose** between comprehensive tool access and efficient
|
|
15
|
+
resource usage
|
|
16
|
+
|
|
17
|
+
This creates a fundamental workflow problem: you either load
|
|
18
|
+
everything (wasting tokens) or manually edit JSON configs before each
|
|
19
|
+
session (time-consuming and error-prone).
|
|
20
|
+
|
|
21
|
+
## The Solution: Session-Specific MCP Configuration Manager
|
|
22
|
+
|
|
23
|
+
A CLI tool that manages your MCP server configurations dynamically by
|
|
24
|
+
manipulating the `.claude.json` file before Claude Code sessions
|
|
25
|
+
start.
|
|
26
|
+
|
|
27
|
+
### Core Concept
|
|
28
|
+
|
|
29
|
+
Instead of fighting Claude Code's static loading, work with it by
|
|
30
|
+
making configuration changes fast and intelligent:
|
|
31
|
+
|
|
32
|
+
1. **Store all available MCP servers** in a separate configuration
|
|
33
|
+
repository
|
|
34
|
+
2. **Select servers per session** using an interactive interface
|
|
35
|
+
3. **Automatically update** `.claude.json` with only selected servers
|
|
36
|
+
4. **Launch Claude Code** with optimized configuration
|
|
37
|
+
5. **Save successful combinations** as reusable presets
|
|
38
|
+
|
|
39
|
+
### Key Features
|
|
40
|
+
|
|
41
|
+
**Interactive Server Selection**
|
|
42
|
+
|
|
43
|
+
- Checkbox interface showing all available MCP servers
|
|
44
|
+
- Real-time token usage estimates for each server
|
|
45
|
+
- Smart warnings when approaching context limits
|
|
46
|
+
- Tag-based filtering (e.g., "web-dev", "data-analysis", "automation")
|
|
47
|
+
|
|
48
|
+
**Preset Management**
|
|
49
|
+
|
|
50
|
+
- Save frequently used server combinations
|
|
51
|
+
- Load preset configurations instantly
|
|
52
|
+
- Project-specific presets (auto-detect based on directory)
|
|
53
|
+
- Share presets with team members
|
|
54
|
+
|
|
55
|
+
**Intelligent Recommendations**
|
|
56
|
+
|
|
57
|
+
- Suggest optimal server combinations for detected project types
|
|
58
|
+
- Learn from usage patterns to recommend relevant tools
|
|
59
|
+
- Context-aware suggestions based on file types in current directory
|
|
60
|
+
|
|
61
|
+
**Seamless Integration**
|
|
62
|
+
|
|
63
|
+
- One command to select servers and launch Claude Code
|
|
64
|
+
- Backup and restore previous configurations
|
|
65
|
+
- Zero impact on existing Claude Code functionality
|
|
66
|
+
|
|
67
|
+
### User Experience
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# Interactive selection for this session
|
|
71
|
+
mcp-manager select
|
|
72
|
+
|
|
73
|
+
# Quick preset loading
|
|
74
|
+
mcp-manager start --preset "web-development"
|
|
75
|
+
|
|
76
|
+
# Enable specific servers by name
|
|
77
|
+
mcp-manager enable context7 github filesystem
|
|
78
|
+
|
|
79
|
+
# Save current successful combination
|
|
80
|
+
mcp-manager save-preset "data-pipeline" --current
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Benefits
|
|
84
|
+
|
|
85
|
+
- **95% token reduction** by loading only needed tools
|
|
86
|
+
- **No session restarts** for configuration changes
|
|
87
|
+
- **Faster startup times** with fewer servers to initialize
|
|
88
|
+
- **Better resource utilization** and longer conversation capacity
|
|
89
|
+
- **Experimentation-friendly** - try new tool combinations easily
|
|
90
|
+
- **Team collaboration** through shared preset configurations
|
|
91
|
+
|
|
92
|
+
### Storage Location
|
|
93
|
+
|
|
94
|
+
Extends existing Claude configuration structure:
|
|
95
|
+
|
|
96
|
+
- Available servers stored in `~/.claude/mcp-manager/`
|
|
97
|
+
- Presets and settings in `~/.claude/settings.json`
|
|
98
|
+
- No modification to Claude Code's core configuration patterns
|
|
99
|
+
|
|
100
|
+
This solution transforms the MCP configuration experience from a
|
|
101
|
+
static, all-or-nothing choice into a dynamic, session-optimized
|
|
102
|
+
workflow that maximizes both functionality and efficiency.
|