mcpick 0.0.11 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # mcpick
2
2
 
3
+ ## 0.0.12
4
+
5
+ ### Patch Changes
6
+
7
+ - b4f38d5: fix: discover externally-added servers and use atomic JSON
8
+ writes
9
+
3
10
  ## 0.0.11
4
11
 
5
12
  ### Patch Changes
@@ -1,5 +1,5 @@
1
1
  import { defineCommand } from 'citty';
2
- import { read_server_registry, write_server_registry, } from '../../core/registry.js';
2
+ import { get_all_available_servers, read_server_registry, write_server_registry, } from '../../core/registry.js';
3
3
  import { remove_mcp_via_cli } from '../../utils/claude-cli.js';
4
4
  import { error } from '../output.js';
5
5
  export default defineCommand({
@@ -15,17 +15,21 @@ export default defineCommand({
15
15
  },
16
16
  },
17
17
  async run({ args }) {
18
+ const all_servers = await get_all_available_servers();
19
+ const found = all_servers.find((s) => s.name === args.server);
20
+ if (!found) {
21
+ error(`Server '${args.server}' not found. Run 'mcpick list' to see available servers.`);
22
+ }
23
+ // Remove from registry if present
18
24
  const registry = await read_server_registry();
19
25
  const index = registry.servers.findIndex((s) => s.name === args.server);
20
- if (index < 0) {
21
- error(`Server '${args.server}' not found in registry. Run 'mcpick list' to see available servers.`);
26
+ if (index >= 0) {
27
+ registry.servers.splice(index, 1);
28
+ await write_server_registry(registry);
22
29
  }
23
- // Remove from registry
24
- registry.servers.splice(index, 1);
25
- await write_server_registry(registry);
26
- // Also disable via CLI (best effort)
30
+ // Remove via CLI
27
31
  await remove_mcp_via_cli(args.server);
28
- console.log(`Removed '${args.server}' from registry`);
32
+ console.log(`Removed '${args.server}'`);
29
33
  },
30
34
  });
31
35
  //# sourceMappingURL=remove.js.map
@@ -1,4 +1,5 @@
1
- import { access, readFile, writeFile } from 'node:fs/promises';
1
+ import { access, readFile } from 'node:fs/promises';
2
+ import { atomic_json_write } from '../utils/atomic-write.js';
2
3
  import { get_claude_config_path, get_current_project_path, get_project_mcp_json_path, } from '../utils/paths.js';
3
4
  import { validate_claude_config } from './validation.js';
4
5
  export async function read_claude_config() {
@@ -19,20 +20,10 @@ export async function read_claude_config() {
19
20
  }
20
21
  }
21
22
  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');
23
+ await atomic_json_write(get_claude_config_path(), (existing) => {
24
+ existing.mcpServers = config.mcpServers;
25
+ return existing;
26
+ });
36
27
  }
37
28
  export function get_enabled_servers(config) {
38
29
  if (!config.mcpServers) {
@@ -1,7 +1,8 @@
1
1
  import { exec } from 'node:child_process';
2
- import { readdir, readFile, rm, writeFile } from 'node:fs/promises';
2
+ import { readdir, readFile, rm } from 'node:fs/promises';
3
3
  import { join, resolve } from 'node:path';
4
4
  import { promisify } from 'node:util';
5
+ import { atomic_json_write } from '../utils/atomic-write.js';
5
6
  import { get_installed_plugins_path, get_known_marketplaces_path, get_marketplace_manifest_path, get_plugin_cache_dir, } from '../utils/paths.js';
6
7
  const execAsync = promisify(exec);
7
8
  const EMPTY_INSTALLED = {
@@ -19,7 +20,7 @@ export async function read_installed_plugins() {
19
20
  }
20
21
  }
21
22
  export async function write_installed_plugins(data) {
22
- await writeFile(get_installed_plugins_path(), JSON.stringify(data, null, 2), 'utf-8');
23
+ await atomic_json_write(get_installed_plugins_path(), () => data);
23
24
  }
24
25
  export async function read_known_marketplaces() {
25
26
  try {
@@ -40,7 +40,17 @@ export async function add_server_to_registry(server) {
40
40
  await write_server_registry(registry);
41
41
  }
42
42
  export async function get_all_available_servers() {
43
+ const { get_enabled_servers, read_claude_config } = await import('./config.js');
43
44
  const registry = await read_server_registry();
45
+ const config = await read_claude_config();
46
+ const config_servers = get_enabled_servers(config);
47
+ // Merge: registry is base, config servers fill in any missing
48
+ const known_names = new Set(registry.servers.map((s) => s.name));
49
+ for (const server of config_servers) {
50
+ if (!known_names.has(server.name)) {
51
+ registry.servers.push(server);
52
+ }
53
+ }
44
54
  return registry.servers;
45
55
  }
46
56
  export async function sync_servers_to_registry(servers) {
@@ -1,4 +1,5 @@
1
- import { access, readFile, writeFile } from 'node:fs/promises';
1
+ import { access, readFile } from 'node:fs/promises';
2
+ import { atomic_json_write } from '../utils/atomic-write.js';
2
3
  import { get_claude_settings_path } from '../utils/paths.js';
3
4
  export async function read_claude_settings() {
4
5
  const settings_path = get_claude_settings_path();
@@ -17,20 +18,12 @@ export async function read_claude_settings() {
17
18
  }
18
19
  }
19
20
  export async function write_claude_settings(updates) {
20
- const settings_path = get_claude_settings_path();
21
- let existing = {};
22
- try {
23
- const content = await readFile(settings_path, 'utf-8');
24
- existing = JSON.parse(content);
25
- }
26
- catch {
27
- // Start with empty if file doesn't exist
28
- }
29
- // Merge only the keys we're updating
30
- for (const [key, value] of Object.entries(updates)) {
31
- existing[key] = value;
32
- }
33
- await writeFile(settings_path, JSON.stringify(existing, null, 2), 'utf-8');
21
+ await atomic_json_write(get_claude_settings_path(), (existing) => {
22
+ for (const [key, value] of Object.entries(updates)) {
23
+ existing[key] = value;
24
+ }
25
+ return existing;
26
+ });
34
27
  }
35
28
  /**
36
29
  * Parse enabledPlugins into structured list.
@@ -0,0 +1,27 @@
1
+ import { readFile, rename, writeFile } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ /**
4
+ * Atomically write a JSON file with fresh-read merging.
5
+ *
6
+ * 1. Re-reads the file right before writing to pick up concurrent changes
7
+ * 2. Applies the merge function to the freshest data
8
+ * 3. Writes to a temp file, then renames (atomic on same filesystem)
9
+ */
10
+ export async function atomic_json_write(file_path, merge) {
11
+ // Read the freshest version right before writing
12
+ let existing = {};
13
+ try {
14
+ const content = await readFile(file_path, 'utf-8');
15
+ existing = JSON.parse(content);
16
+ }
17
+ catch {
18
+ // File doesn't exist or invalid — start fresh
19
+ }
20
+ const merged = merge(existing);
21
+ const content = JSON.stringify(merged, null, 2);
22
+ // Write to temp file then rename for atomicity
23
+ const tmp_path = join(dirname(file_path), `.${Date.now()}.tmp`);
24
+ await writeFile(tmp_path, content, 'utf-8');
25
+ await rename(tmp_path, file_path);
26
+ }
27
+ //# sourceMappingURL=atomic-write.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcpick",
3
- "version": "0.0.11",
3
+ "version": "0.0.12",
4
4
  "description": "Dynamic MCP server and plugin configuration manager for Claude Code",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",