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 ADDED
@@ -0,0 +1,7 @@
1
+ # mcpick
2
+
3
+ ## 0.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - debe3ad: initial release
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -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.