mcpick 0.0.17 → 0.0.18

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,13 @@
1
1
  # mcpick
2
2
 
3
+ ## 0.0.18
4
+
5
+ ### Patch Changes
6
+
7
+ - 58ff00f: Add clone command and skip redundant stdio transport flag
8
+ - 7f277da: redact env keys when listing
9
+ - b52fcbd: Show available CLI commands hint in TUI intro
10
+
3
11
  ## 0.0.17
4
12
 
5
13
  ### Patch Changes
@@ -0,0 +1,108 @@
1
+ import { defineCommand } from 'citty';
2
+ import { detect_server_scope, find_server_in_scope, } from '../../core/config.js';
3
+ import { add_server_to_registry } from '../../core/registry.js';
4
+ import { validate_mcp_server } from '../../core/validation.js';
5
+ import { add_mcp_via_cli } from '../../utils/claude-cli.js';
6
+ import { redact_server } from '../../utils/redact.js';
7
+ import { error, output } from '../output.js';
8
+ export default defineCommand({
9
+ meta: {
10
+ name: 'clone',
11
+ description: 'Clone an existing MCP server config with a new name, optionally overriding command/args',
12
+ },
13
+ args: {
14
+ source: {
15
+ type: 'positional',
16
+ description: 'Source server name to clone from',
17
+ required: true,
18
+ },
19
+ name: {
20
+ type: 'positional',
21
+ description: 'New server name',
22
+ required: true,
23
+ },
24
+ command: {
25
+ type: 'string',
26
+ description: 'Override command (e.g. "node" for local dev)',
27
+ },
28
+ args: {
29
+ type: 'string',
30
+ description: 'Override comma-separated arguments',
31
+ },
32
+ scope: {
33
+ type: 'string',
34
+ description: 'Scope for new server (default: same as source)',
35
+ },
36
+ json: {
37
+ type: 'boolean',
38
+ description: 'Output as JSON',
39
+ default: false,
40
+ },
41
+ },
42
+ async run({ args }) {
43
+ if (args.scope &&
44
+ !['local', 'project', 'user'].includes(args.scope)) {
45
+ error(`Invalid scope: ${args.scope}. Use local, project, or user.`);
46
+ }
47
+ // Find the source server
48
+ const scope = args.scope;
49
+ let found;
50
+ if (scope) {
51
+ found = await find_server_in_scope(args.source, scope);
52
+ }
53
+ else {
54
+ found = await detect_server_scope(args.source);
55
+ }
56
+ if (!found) {
57
+ error(`Server '${args.source}' not found${scope ? ` in ${scope} scope` : ' in any scope'}`);
58
+ }
59
+ const target_scope = args.scope || found.scope;
60
+ // Clone the config, applying overrides
61
+ const cloned = {
62
+ ...found.server,
63
+ name: args.name,
64
+ };
65
+ if (args.command) {
66
+ cloned.command = args.command;
67
+ // When overriding command, clear url/type if switching from http/sse to stdio
68
+ delete cloned.url;
69
+ if (cloned.type === 'sse' || cloned.type === 'http') {
70
+ delete cloned.type;
71
+ }
72
+ }
73
+ if (args.args) {
74
+ cloned.args = args.args.split(',');
75
+ }
76
+ // Validate
77
+ let server;
78
+ try {
79
+ server = validate_mcp_server(cloned);
80
+ }
81
+ catch (err) {
82
+ error(`Invalid cloned config: ${err instanceof Error ? err.message : 'validation failed'}`);
83
+ }
84
+ // Add to registry
85
+ await add_server_to_registry(server);
86
+ // Enable via CLI
87
+ const result = await add_mcp_via_cli(server, target_scope);
88
+ if (args.json) {
89
+ output({
90
+ cloned: server.name,
91
+ from: args.source,
92
+ scope: target_scope,
93
+ server: redact_server(server),
94
+ cli: result.success,
95
+ error: result.error,
96
+ }, true);
97
+ }
98
+ else {
99
+ if (result.success) {
100
+ console.log(`Cloned '${args.source}' → '${server.name}' (scope: ${target_scope})`);
101
+ }
102
+ else {
103
+ console.log(`Cloned '${args.source}' → '${server.name}' to registry but CLI failed: ${result.error}`);
104
+ }
105
+ }
106
+ },
107
+ });
108
+ //# sourceMappingURL=clone.js.map
@@ -1,5 +1,6 @@
1
1
  import { defineCommand } from 'citty';
2
2
  import { apply_dev_override, list_dev_overrides, restore_all_dev_overrides, restore_dev_override, } from '../../core/dev-override.js';
3
+ import { redact_server_base } from '../../utils/redact.js';
3
4
  import { error, output } from '../output.js';
4
5
  const apply = defineCommand({
5
6
  meta: {
@@ -126,7 +127,12 @@ const list = defineCommand({
126
127
  async run({ args }) {
127
128
  const overrides = await list_dev_overrides();
128
129
  if (args.json) {
129
- output(overrides, true);
130
+ const redacted = overrides.map((o) => ({
131
+ ...o,
132
+ original: redact_server_base(o.original),
133
+ dev: redact_server_base(o.dev),
134
+ }));
135
+ output(redacted, true);
130
136
  return;
131
137
  }
132
138
  if (overrides.length === 0) {
@@ -1,6 +1,7 @@
1
1
  import { defineCommand } from 'citty';
2
2
  import { get_enabled_servers_for_scope } from '../../core/config.js';
3
3
  import { get_all_available_servers } from '../../core/registry.js';
4
+ import { redact_server } from '../../utils/redact.js';
4
5
  import { error, output } from '../output.js';
5
6
  export default defineCommand({
6
7
  meta: {
@@ -38,7 +39,7 @@ export default defineCommand({
38
39
  for (const scope of scopes) {
39
40
  status[scope] = enabled_by_scope[scope].includes(server.name);
40
41
  }
41
- const { name, ...rest } = server;
42
+ const { name, ...rest } = redact_server(server);
42
43
  return { name, ...status, ...rest };
43
44
  });
44
45
  output(data, true);
package/dist/cli/index.js CHANGED
@@ -11,6 +11,7 @@ const main = defineCommand({
11
11
  remove: () => import('./commands/remove.js').then((m) => m.default),
12
12
  add: () => import('./commands/add.js').then((m) => m.default),
13
13
  'add-json': () => import('./commands/add-json.js').then((m) => m.default),
14
+ clone: () => import('./commands/clone.js').then((m) => m.default),
14
15
  get: () => import('./commands/get.js').then((m) => m.default),
15
16
  'reset-project-choices': () => import('./commands/reset-project-choices.js').then((m) => m.default),
16
17
  backup: () => import('./commands/backup.js').then((m) => m.default),
@@ -42,6 +42,60 @@ export function create_config_from_servers(selected_servers) {
42
42
  });
43
43
  return { mcpServers: mcp_servers };
44
44
  }
45
+ /**
46
+ * Find a server's full config in a specific scope.
47
+ */
48
+ export async function find_server_in_scope(name, scope) {
49
+ if (scope === 'user' || scope === 'local') {
50
+ const config_path = get_claude_config_path();
51
+ try {
52
+ await access(config_path);
53
+ const content = await readFile(config_path, 'utf-8');
54
+ const parsed = JSON.parse(content);
55
+ if (scope === 'user') {
56
+ const server = parsed.mcpServers?.[name];
57
+ if (server)
58
+ return { server, scope: 'user' };
59
+ }
60
+ else {
61
+ const cwd = get_current_project_path();
62
+ const server = parsed.projects?.[cwd]?.mcpServers?.[name];
63
+ if (server)
64
+ return { server, scope: 'local' };
65
+ }
66
+ }
67
+ catch {
68
+ // File doesn't exist
69
+ }
70
+ }
71
+ else if (scope === 'project') {
72
+ const mcp_path = get_project_mcp_json_path();
73
+ try {
74
+ await access(mcp_path);
75
+ const content = await readFile(mcp_path, 'utf-8');
76
+ const parsed = JSON.parse(content);
77
+ const server = parsed.mcpServers?.[name];
78
+ if (server)
79
+ return { server, scope: 'project' };
80
+ }
81
+ catch {
82
+ // File doesn't exist
83
+ }
84
+ }
85
+ return null;
86
+ }
87
+ /**
88
+ * Auto-detect which scope a server lives in.
89
+ * Searches local → project → user.
90
+ */
91
+ export async function detect_server_scope(name) {
92
+ for (const scope of ['local', 'project', 'user']) {
93
+ const result = await find_server_in_scope(name, scope);
94
+ if (result)
95
+ return result;
96
+ }
97
+ return null;
98
+ }
45
99
  /**
46
100
  * Read full Claude config including projects section
47
101
  */
@@ -1,6 +1,7 @@
1
- import { access, readFile } from 'node:fs/promises';
1
+ import { readFile } from 'node:fs/promises';
2
2
  import { atomic_json_write } from '../utils/atomic-write.js';
3
3
  import { get_claude_config_path, get_current_project_path, get_dev_overrides_path, get_project_mcp_json_path, } from '../utils/paths.js';
4
+ import { detect_server_scope, find_server_in_scope, } from './config.js';
4
5
  const EMPTY_OVERRIDES = {
5
6
  version: 1,
6
7
  overrides: {},
@@ -17,62 +18,6 @@ export async function read_dev_overrides() {
17
18
  async function write_dev_overrides(data) {
18
19
  await atomic_json_write(get_dev_overrides_path(), () => data);
19
20
  }
20
- /**
21
- * Read full config for a given scope and return the server entry if found.
22
- * Returns { config, server, scope } or null.
23
- */
24
- async function find_server_in_scope(name, scope) {
25
- if (scope === 'user' || scope === 'local') {
26
- const config_path = get_claude_config_path();
27
- try {
28
- await access(config_path);
29
- const content = await readFile(config_path, 'utf-8');
30
- const parsed = JSON.parse(content);
31
- if (scope === 'user') {
32
- const server = parsed.mcpServers?.[name];
33
- if (server)
34
- return { server, scope: 'user' };
35
- }
36
- else {
37
- // local scope: projects[cwd].mcpServers
38
- const cwd = get_current_project_path();
39
- const server = parsed.projects?.[cwd]?.mcpServers?.[name];
40
- if (server)
41
- return { server, scope: 'local' };
42
- }
43
- }
44
- catch {
45
- // File doesn't exist
46
- }
47
- }
48
- else if (scope === 'project') {
49
- const mcp_path = get_project_mcp_json_path();
50
- try {
51
- await access(mcp_path);
52
- const content = await readFile(mcp_path, 'utf-8');
53
- const parsed = JSON.parse(content);
54
- const server = parsed.mcpServers?.[name];
55
- if (server)
56
- return { server, scope: 'project' };
57
- }
58
- catch {
59
- // File doesn't exist
60
- }
61
- }
62
- return null;
63
- }
64
- /**
65
- * Auto-detect which scope a server lives in.
66
- * Searches local → project → user.
67
- */
68
- async function detect_server_scope(name) {
69
- for (const scope of ['local', 'project', 'user']) {
70
- const result = await find_server_in_scope(name, scope);
71
- if (result)
72
- return result;
73
- }
74
- return null;
75
- }
76
21
  /**
77
22
  * Write a server config into the appropriate scope config file.
78
23
  */
package/dist/index.js CHANGED
@@ -165,6 +165,7 @@ async function main() {
165
165
  return;
166
166
  }
167
167
  intro('MCPick - MCP Server Configuration Manager');
168
+ log.info('CLI: mcpick <command> --help | Commands: list, add, clone, enable, disable, remove, get, dev, profile, plugins, hooks, backup, restore');
168
169
  while (true) {
169
170
  try {
170
171
  const action = await select({
@@ -295,6 +296,7 @@ const SUBCOMMANDS = new Set([
295
296
  'remove',
296
297
  'add',
297
298
  'add-json',
299
+ 'clone',
298
300
  'get',
299
301
  'reset-project-choices',
300
302
  'hooks',
@@ -34,9 +34,11 @@ function build_add_command(server, scope) {
34
34
  const parts = ['claude', 'mcp', 'add'];
35
35
  // Server name
36
36
  parts.push(shell_escape(server.name));
37
- // Transport type
37
+ // Transport type (only specify if non-default)
38
38
  const transport = server.type || 'stdio';
39
- parts.push('--transport', transport);
39
+ if (transport !== 'stdio') {
40
+ parts.push('--transport', transport);
41
+ }
40
42
  // Scope
41
43
  parts.push('--scope', scope);
42
44
  // Handle different transport types
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Redact sensitive values (env, headers) from a server config.
3
+ * Shows key names but replaces values with "***".
4
+ */
5
+ export function redact_server_base(server) {
6
+ const redacted = { ...server };
7
+ if ('env' in redacted && redacted.env) {
8
+ redacted.env = redact_record(redacted.env);
9
+ }
10
+ if ('headers' in redacted && redacted.headers) {
11
+ redacted.headers = redact_record(redacted.headers);
12
+ }
13
+ return redacted;
14
+ }
15
+ export function redact_server(server) {
16
+ return {
17
+ ...redact_server_base(server),
18
+ name: server.name,
19
+ };
20
+ }
21
+ function redact_record(record) {
22
+ const redacted = {};
23
+ for (const key of Object.keys(record)) {
24
+ redacted[key] = '***';
25
+ }
26
+ return redacted;
27
+ }
28
+ //# sourceMappingURL=redact.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcpick",
3
- "version": "0.0.17",
3
+ "version": "0.0.18",
4
4
  "description": "Dynamic MCP server and plugin configuration manager for Claude Code",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",